mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
chore: Convert ErrorReporting to a Service to use DI. Add some tests (no-changelog) (#11279)
This commit is contained in:
parent
5300e0ac45
commit
73145b70b8
|
@ -1,60 +0,0 @@
|
||||||
import { GlobalConfig } from '@n8n/config';
|
|
||||||
import type { ClientOptions, ErrorEvent } from '@sentry/types';
|
|
||||||
import { strict as assert } from 'node:assert';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
|
|
||||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
|
||||||
|
|
||||||
const init = jest.fn();
|
|
||||||
|
|
||||||
jest.mock('@sentry/node', () => ({
|
|
||||||
init,
|
|
||||||
setTag: jest.fn(),
|
|
||||||
captureException: jest.fn(),
|
|
||||||
Integrations: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.spyOn(process, 'on');
|
|
||||||
|
|
||||||
describe('initErrorHandling', () => {
|
|
||||||
let beforeSend: ClientOptions['beforeSend'];
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
Container.get(GlobalConfig).sentry.backendDsn = 'backend-dsn';
|
|
||||||
const errorReporting = require('@/error-reporting');
|
|
||||||
await errorReporting.initErrorHandling();
|
|
||||||
const options = (init.mock.calls[0] as [ClientOptions])[0];
|
|
||||||
beforeSend = options.beforeSend;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores errors with level warning', async () => {
|
|
||||||
const originalException = new InternalServerError('test');
|
|
||||||
originalException.level = 'warning';
|
|
||||||
|
|
||||||
const event = {} as ErrorEvent;
|
|
||||||
|
|
||||||
assert(beforeSend);
|
|
||||||
expect(await beforeSend(event, { originalException })).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps events with a cause with error level', async () => {
|
|
||||||
const cause = new Error('cause-error');
|
|
||||||
|
|
||||||
const originalException = new InternalServerError('test', cause);
|
|
||||||
const event = {} as ErrorEvent;
|
|
||||||
|
|
||||||
assert(beforeSend);
|
|
||||||
expect(await beforeSend(event, { originalException })).toEqual(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores events with error cause with warning level', async () => {
|
|
||||||
const cause: Error & { level?: 'warning' } = new Error('cause-error');
|
|
||||||
cause.level = 'warning';
|
|
||||||
|
|
||||||
const originalException = new InternalServerError('test', cause);
|
|
||||||
const event = {} as ErrorEvent;
|
|
||||||
|
|
||||||
assert(beforeSend);
|
|
||||||
expect(await beforeSend(event, { originalException })).toEqual(null);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -8,7 +8,7 @@ describe('LoadNodesAndCredentials', () => {
|
||||||
let instance: LoadNodesAndCredentials;
|
let instance: LoadNodesAndCredentials;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock());
|
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
|
||||||
instance.loaders.package1 = mock<DirectoryLoader>({
|
instance.loaders.package1 = mock<DirectoryLoader>({
|
||||||
directory: '/icons/package1',
|
directory: '/icons/package1',
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActiveWorkflows,
|
ActiveWorkflows,
|
||||||
|
ErrorReporter,
|
||||||
InstanceSettings,
|
InstanceSettings,
|
||||||
NodeExecuteFunctions,
|
NodeExecuteFunctions,
|
||||||
PollContext,
|
PollContext,
|
||||||
|
@ -25,7 +26,6 @@ import type {
|
||||||
import {
|
import {
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowActivationError,
|
WorkflowActivationError,
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
WebhookPathTakenError,
|
WebhookPathTakenError,
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
@ -41,10 +41,12 @@ import {
|
||||||
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
import { OnShutdown } from '@/decorators/on-shutdown';
|
import { OnShutdown } from '@/decorators/on-shutdown';
|
||||||
|
import { ExecutionService } from '@/executions/execution.service';
|
||||||
import { ExternalHooks } from '@/external-hooks';
|
import { ExternalHooks } from '@/external-hooks';
|
||||||
import type { IWorkflowDb } from '@/interfaces';
|
import type { IWorkflowDb } from '@/interfaces';
|
||||||
import { Logger } from '@/logging/logger.service';
|
import { Logger } from '@/logging/logger.service';
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
|
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||||
import { ActiveWorkflowsService } from '@/services/active-workflows.service';
|
import { ActiveWorkflowsService } from '@/services/active-workflows.service';
|
||||||
import { OrchestrationService } from '@/services/orchestration.service';
|
import { OrchestrationService } from '@/services/orchestration.service';
|
||||||
import * as WebhookHelpers from '@/webhooks/webhook-helpers';
|
import * as WebhookHelpers from '@/webhooks/webhook-helpers';
|
||||||
|
@ -53,9 +55,6 @@ import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-da
|
||||||
import { WorkflowExecutionService } from '@/workflows/workflow-execution.service';
|
import { WorkflowExecutionService } from '@/workflows/workflow-execution.service';
|
||||||
import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service';
|
import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service';
|
||||||
|
|
||||||
import { ExecutionService } from './executions/execution.service';
|
|
||||||
import { Publisher } from './scaling/pubsub/publisher.service';
|
|
||||||
|
|
||||||
interface QueuedActivation {
|
interface QueuedActivation {
|
||||||
activationMode: WorkflowActivateMode;
|
activationMode: WorkflowActivateMode;
|
||||||
lastTimeout: number;
|
lastTimeout: number;
|
||||||
|
@ -69,6 +68,7 @@ export class ActiveWorkflowManager {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly activeWorkflows: ActiveWorkflows,
|
private readonly activeWorkflows: ActiveWorkflows,
|
||||||
private readonly activeExecutions: ActiveExecutions,
|
private readonly activeExecutions: ActiveExecutions,
|
||||||
private readonly externalHooks: ExternalHooks,
|
private readonly externalHooks: ExternalHooks,
|
||||||
|
@ -205,7 +205,7 @@ export class ActiveWorkflowManager {
|
||||||
try {
|
try {
|
||||||
await this.clearWebhooks(workflow.id);
|
await this.clearWebhooks(workflow.id);
|
||||||
} catch (error1) {
|
} catch (error1) {
|
||||||
ErrorReporter.error(error1);
|
this.errorReporter.error(error1);
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Could not remove webhooks of workflow "${workflow.id}" because of error: "${error1.message}"`,
|
`Could not remove webhooks of workflow "${workflow.id}" because of error: "${error1.message}"`,
|
||||||
);
|
);
|
||||||
|
@ -439,7 +439,7 @@ export class ActiveWorkflowManager {
|
||||||
this.logger.info(' => Started');
|
this.logger.info(' => Started');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
this.errorReporter.error(error);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
' => ERROR: Workflow could not be activated on first try, keep on trying if not an auth issue',
|
' => ERROR: Workflow could not be activated on first try, keep on trying if not an auth issue',
|
||||||
);
|
);
|
||||||
|
@ -635,7 +635,7 @@ export class ActiveWorkflowManager {
|
||||||
try {
|
try {
|
||||||
await this.add(workflowId, activationMode, workflowData);
|
await this.add(workflowId, activationMode, workflowData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
this.errorReporter.error(error);
|
||||||
let lastTimeout = this.queuedActivations[workflowId].lastTimeout;
|
let lastTimeout = this.queuedActivations[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);
|
||||||
|
@ -707,7 +707,7 @@ export class ActiveWorkflowManager {
|
||||||
try {
|
try {
|
||||||
await this.clearWebhooks(workflowId);
|
await this.clearWebhooks(workflowId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
this.errorReporter.error(error);
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`,
|
`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`,
|
||||||
);
|
);
|
||||||
|
@ -724,7 +724,7 @@ export class ActiveWorkflowManager {
|
||||||
try {
|
try {
|
||||||
await this.clearWebhooks(workflowId);
|
await this.clearWebhooks(workflowId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
this.errorReporter.error(error);
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`,
|
`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { PushPayload } from '@n8n/api-types';
|
import type { PushPayload } from '@n8n/api-types';
|
||||||
|
import { ErrorReporter } from 'n8n-core';
|
||||||
import type { Workflow } from 'n8n-workflow';
|
import type { Workflow } from 'n8n-workflow';
|
||||||
import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow';
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
import { CollaborationState } from '@/collaboration/collaboration.state';
|
import { CollaborationState } from '@/collaboration/collaboration.state';
|
||||||
|
@ -20,6 +21,7 @@ import { parseWorkflowMessage } from './collaboration.message';
|
||||||
@Service()
|
@Service()
|
||||||
export class CollaborationService {
|
export class CollaborationService {
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly push: Push,
|
private readonly push: Push,
|
||||||
private readonly state: CollaborationState,
|
private readonly state: CollaborationState,
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
|
@ -31,7 +33,7 @@ export class CollaborationService {
|
||||||
try {
|
try {
|
||||||
await this.handleUserMessage(event.userId, event.msg);
|
await this.handleUserMessage(event.userId, event.msg);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporterProxy.error(
|
this.errorReporter.error(
|
||||||
new ApplicationError('Error handling CollaborationService push message', {
|
new ApplicationError('Error handling CollaborationService push message', {
|
||||||
extra: {
|
extra: {
|
||||||
msg: event.msg,
|
msg: event.msg,
|
||||||
|
|
|
@ -6,13 +6,9 @@ import {
|
||||||
InstanceSettings,
|
InstanceSettings,
|
||||||
ObjectStoreService,
|
ObjectStoreService,
|
||||||
DataDeduplicationService,
|
DataDeduplicationService,
|
||||||
|
ErrorReporter,
|
||||||
} from 'n8n-core';
|
} from 'n8n-core';
|
||||||
import {
|
import { ApplicationError, ensureError, sleep } from 'n8n-workflow';
|
||||||
ApplicationError,
|
|
||||||
ensureError,
|
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
sleep,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
import type { AbstractServer } from '@/abstract-server';
|
import type { AbstractServer } from '@/abstract-server';
|
||||||
|
@ -22,7 +18,6 @@ import * as CrashJournal from '@/crash-journal';
|
||||||
import * as Db from '@/db';
|
import * as Db from '@/db';
|
||||||
import { getDataDeduplicationService } from '@/deduplication';
|
import { getDataDeduplicationService } from '@/deduplication';
|
||||||
import { DeprecationService } from '@/deprecation/deprecation.service';
|
import { DeprecationService } from '@/deprecation/deprecation.service';
|
||||||
import { initErrorHandling } from '@/error-reporting';
|
|
||||||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||||
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
||||||
import { initExpressionEvaluator } from '@/expression-evaluator';
|
import { initExpressionEvaluator } from '@/expression-evaluator';
|
||||||
|
@ -39,6 +34,8 @@ import { WorkflowHistoryManager } from '@/workflows/workflow-history/workflow-hi
|
||||||
export abstract class BaseCommand extends Command {
|
export abstract class BaseCommand extends Command {
|
||||||
protected logger = Container.get(Logger);
|
protected logger = Container.get(Logger);
|
||||||
|
|
||||||
|
protected readonly errorReporter = Container.get(ErrorReporter);
|
||||||
|
|
||||||
protected externalHooks?: ExternalHooks;
|
protected externalHooks?: ExternalHooks;
|
||||||
|
|
||||||
protected nodeTypes: NodeTypes;
|
protected nodeTypes: NodeTypes;
|
||||||
|
@ -63,7 +60,7 @@ export abstract class BaseCommand extends Command {
|
||||||
protected needsCommunityPackages = false;
|
protected needsCommunityPackages = false;
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
await initErrorHandling();
|
await this.errorReporter.init();
|
||||||
initExpressionEvaluator();
|
initExpressionEvaluator();
|
||||||
|
|
||||||
process.once('SIGTERM', this.onTerminationSignal('SIGTERM'));
|
process.once('SIGTERM', this.onTerminationSignal('SIGTERM'));
|
||||||
|
@ -130,7 +127,7 @@ export abstract class BaseCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async exitWithCrash(message: string, error: unknown) {
|
protected async exitWithCrash(message: string, error: unknown) {
|
||||||
ErrorReporter.error(new Error(message, { cause: error }), { level: 'fatal' });
|
this.errorReporter.error(new Error(message, { cause: error }), { level: 'fatal' });
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import fs from 'fs';
|
||||||
import { diff } from 'json-diff';
|
import { diff } from 'json-diff';
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
import type { IRun, ITaskData, IWorkflowExecutionDataProcess } from 'n8n-workflow';
|
import type { IRun, ITaskData, IWorkflowExecutionDataProcess } from 'n8n-workflow';
|
||||||
import { ApplicationError, jsonParse, ErrorReporterProxy } from 'n8n-workflow';
|
import { ApplicationError, jsonParse } from 'n8n-workflow';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { sep } from 'path';
|
import { sep } from 'path';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
@ -822,7 +822,7 @@ export class ExecuteBatch extends BaseCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorReporterProxy.error(e, {
|
this.errorReporter.error(e, {
|
||||||
extra: {
|
extra: {
|
||||||
workflowId: workflowData.id,
|
workflowId: workflowData.id,
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,12 +21,8 @@ import {
|
||||||
import { DateUtils } from '@n8n/typeorm/util/DateUtils';
|
import { DateUtils } from '@n8n/typeorm/util/DateUtils';
|
||||||
import { parse, stringify } from 'flatted';
|
import { parse, stringify } from 'flatted';
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
import { BinaryDataService } from 'n8n-core';
|
import { BinaryDataService, ErrorReporter } from 'n8n-core';
|
||||||
import {
|
import { ExecutionCancelledError, ApplicationError } from 'n8n-workflow';
|
||||||
ExecutionCancelledError,
|
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
ApplicationError,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import type {
|
import type {
|
||||||
AnnotationVote,
|
AnnotationVote,
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
|
@ -125,6 +121,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
dataSource: DataSource,
|
dataSource: DataSource,
|
||||||
private readonly globalConfig: GlobalConfig,
|
private readonly globalConfig: GlobalConfig,
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly executionDataRepository: ExecutionDataRepository,
|
private readonly executionDataRepository: ExecutionDataRepository,
|
||||||
private readonly binaryDataService: BinaryDataService,
|
private readonly binaryDataService: BinaryDataService,
|
||||||
) {
|
) {
|
||||||
|
@ -209,7 +206,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
reportInvalidExecutions(executions: ExecutionEntity[]) {
|
reportInvalidExecutions(executions: ExecutionEntity[]) {
|
||||||
if (executions.length === 0) return;
|
if (executions.length === 0) return;
|
||||||
|
|
||||||
ErrorReporter.error(
|
this.errorReporter.error(
|
||||||
new ApplicationError('Found executions without executionData', {
|
new ApplicationError('Found executions without executionData', {
|
||||||
extra: { executionIds: executions.map(({ id }) => id) },
|
extra: { executionIds: executions.map(({ id }) => id) },
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { DataSource, Repository } from '@n8n/typeorm';
|
import { DataSource, Repository } from '@n8n/typeorm';
|
||||||
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
import { ErrorReporter } from 'n8n-core';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
@ -9,7 +9,10 @@ import { Settings } from '../entities/settings';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SettingsRepository extends Repository<Settings> {
|
export class SettingsRepository extends Repository<Settings> {
|
||||||
constructor(dataSource: DataSource) {
|
constructor(
|
||||||
|
dataSource: DataSource,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
|
) {
|
||||||
super(Settings, dataSource.manager);
|
super(Settings, dataSource.manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@ export class SettingsRepository extends Repository<Settings> {
|
||||||
config.set(key, value);
|
config.set(key, value);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
this.errorReporter.error(error);
|
||||||
}
|
}
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { EntitySubscriberInterface, UpdateEvent } from '@n8n/typeorm';
|
import type { EntitySubscriberInterface, UpdateEvent } from '@n8n/typeorm';
|
||||||
import { EventSubscriber } from '@n8n/typeorm';
|
import { EventSubscriber } from '@n8n/typeorm';
|
||||||
import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow';
|
import { ErrorReporter } from 'n8n-core';
|
||||||
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
import { Logger } from '@/logging/logger.service';
|
import { Logger } from '@/logging/logger.service';
|
||||||
|
@ -11,6 +12,8 @@ import { UserRepository } from '../repositories/user.repository';
|
||||||
|
|
||||||
@EventSubscriber()
|
@EventSubscriber()
|
||||||
export class UserSubscriber implements EntitySubscriberInterface<User> {
|
export class UserSubscriber implements EntitySubscriberInterface<User> {
|
||||||
|
private readonly eventReporter = Container.get(ErrorReporter);
|
||||||
|
|
||||||
listenTo() {
|
listenTo() {
|
||||||
return User;
|
return User;
|
||||||
}
|
}
|
||||||
|
@ -47,7 +50,7 @@ export class UserSubscriber implements EntitySubscriberInterface<User> {
|
||||||
const message = "Could not update the personal project's name";
|
const message = "Could not update the personal project's name";
|
||||||
Container.get(Logger).warn(message, event.entity);
|
Container.get(Logger).warn(message, event.entity);
|
||||||
const exception = new ApplicationError(message);
|
const exception = new ApplicationError(message);
|
||||||
ErrorReporterProxy.warn(exception, event.entity);
|
this.eventReporter.warn(exception, event.entity);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +72,7 @@ export class UserSubscriber implements EntitySubscriberInterface<User> {
|
||||||
const message = "Could not update the personal project's name";
|
const message = "Could not update the personal project's name";
|
||||||
Container.get(Logger).warn(message, event.entity);
|
Container.get(Logger).warn(message, event.entity);
|
||||||
const exception = new ApplicationError(message);
|
const exception = new ApplicationError(message);
|
||||||
ErrorReporterProxy.warn(exception, event.entity);
|
this.eventReporter.warn(exception, event.entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,8 @@
|
||||||
import type { EntityManager } from '@n8n/typeorm';
|
import type { EntityManager } from '@n8n/typeorm';
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import { DataSource as Connection } from '@n8n/typeorm';
|
import { DataSource as Connection } from '@n8n/typeorm';
|
||||||
import {
|
import { ErrorReporter } from 'n8n-core';
|
||||||
DbConnectionTimeoutError,
|
import { DbConnectionTimeoutError, ensureError } from 'n8n-workflow';
|
||||||
ensureError,
|
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
import { inTest } from '@/constants';
|
import { inTest } from '@/constants';
|
||||||
|
@ -38,7 +35,7 @@ if (!inTest) {
|
||||||
connectionState.connected = true;
|
connectionState.connected = true;
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
Container.get(ErrorReporter).error(error);
|
||||||
} finally {
|
} finally {
|
||||||
pingTimer = setTimeout(pingDBFn, 2000);
|
pingTimer = setTimeout(pingDBFn, 2000);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ describe('OnShutdown', () => {
|
||||||
let shutdownService: ShutdownService;
|
let shutdownService: ShutdownService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
shutdownService = new ShutdownService(mock());
|
shutdownService = new ShutdownService(mock(), mock());
|
||||||
Container.set(ShutdownService, shutdownService);
|
Container.set(ShutdownService, shutdownService);
|
||||||
jest.spyOn(shutdownService, 'register');
|
jest.spyOn(shutdownService, 'register');
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import { In } from '@n8n/typeorm';
|
import { In } from '@n8n/typeorm';
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
import { Credentials, InstanceSettings } from 'n8n-core';
|
import { Credentials, ErrorReporter, InstanceSettings } from 'n8n-core';
|
||||||
import {
|
import { ApplicationError, jsonParse, ensureError } from 'n8n-workflow';
|
||||||
ApplicationError,
|
|
||||||
jsonParse,
|
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
ensureError,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { readFile as fsReadFile } from 'node:fs/promises';
|
import { readFile as fsReadFile } from 'node:fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Container, Service } from 'typedi';
|
import { Container, Service } from 'typedi';
|
||||||
|
@ -56,6 +51,7 @@ export class SourceControlImportService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly variablesService: VariablesService,
|
private readonly variablesService: VariablesService,
|
||||||
private readonly activeWorkflowManager: ActiveWorkflowManager,
|
private readonly activeWorkflowManager: ActiveWorkflowManager,
|
||||||
private readonly tagRepository: TagRepository,
|
private readonly tagRepository: TagRepository,
|
||||||
|
@ -104,7 +100,7 @@ export class SourceControlImportService {
|
||||||
if (local.updatedAt instanceof Date) {
|
if (local.updatedAt instanceof Date) {
|
||||||
updatedAt = local.updatedAt;
|
updatedAt = local.updatedAt;
|
||||||
} else {
|
} else {
|
||||||
ErrorReporter.warn('updatedAt is not a Date', {
|
this.errorReporter.warn('updatedAt is not a Date', {
|
||||||
extra: {
|
extra: {
|
||||||
type: typeof local.updatedAt,
|
type: typeof local.updatedAt,
|
||||||
value: local.updatedAt,
|
value: local.updatedAt,
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
import { GlobalConfig } from '@n8n/config';
|
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
|
||||||
import { QueryFailedError } from '@n8n/typeorm';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { createHash } from 'crypto';
|
|
||||||
import { InstanceSettings } from 'n8n-core';
|
|
||||||
import { ErrorReporterProxy, ApplicationError } from 'n8n-workflow';
|
|
||||||
import Container from 'typedi';
|
|
||||||
|
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
export const initErrorHandling = async () => {
|
|
||||||
if (initialized) return;
|
|
||||||
|
|
||||||
process.on('uncaughtException', (error) => {
|
|
||||||
ErrorReporterProxy.error(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
const dsn = Container.get(GlobalConfig).sentry.backendDsn;
|
|
||||||
if (!dsn) {
|
|
||||||
initialized = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect longer stacktraces
|
|
||||||
Error.stackTraceLimit = 50;
|
|
||||||
|
|
||||||
const {
|
|
||||||
N8N_VERSION: release,
|
|
||||||
ENVIRONMENT: environment,
|
|
||||||
DEPLOYMENT_NAME: serverName,
|
|
||||||
} = process.env;
|
|
||||||
|
|
||||||
const { init, captureException, setTag } = await import('@sentry/node');
|
|
||||||
|
|
||||||
const { requestDataIntegration, rewriteFramesIntegration } = await import('@sentry/node');
|
|
||||||
|
|
||||||
const enabledIntegrations = [
|
|
||||||
'InboundFilters',
|
|
||||||
'FunctionToString',
|
|
||||||
'LinkedErrors',
|
|
||||||
'OnUnhandledRejection',
|
|
||||||
'ContextLines',
|
|
||||||
];
|
|
||||||
const seenErrors = new Set<string>();
|
|
||||||
|
|
||||||
init({
|
|
||||||
dsn,
|
|
||||||
release,
|
|
||||||
environment,
|
|
||||||
enableTracing: false,
|
|
||||||
serverName,
|
|
||||||
beforeBreadcrumb: () => null,
|
|
||||||
integrations: (integrations) => [
|
|
||||||
...integrations.filter(({ name }) => enabledIntegrations.includes(name)),
|
|
||||||
rewriteFramesIntegration({ root: process.cwd() }),
|
|
||||||
requestDataIntegration({
|
|
||||||
include: {
|
|
||||||
cookies: false,
|
|
||||||
data: false,
|
|
||||||
headers: false,
|
|
||||||
query_string: false,
|
|
||||||
url: true,
|
|
||||||
user: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
async beforeSend(event, { originalException }) {
|
|
||||||
if (!originalException) return null;
|
|
||||||
|
|
||||||
if (originalException instanceof Promise) {
|
|
||||||
originalException = await originalException.catch((error) => error as Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalException instanceof AxiosError) return null;
|
|
||||||
|
|
||||||
if (
|
|
||||||
originalException instanceof QueryFailedError &&
|
|
||||||
['SQLITE_FULL', 'SQLITE_IOERR'].some((errMsg) => originalException.message.includes(errMsg))
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalException instanceof ApplicationError) {
|
|
||||||
const { level, extra, tags } = originalException;
|
|
||||||
if (level === 'warning') return null;
|
|
||||||
event.level = level;
|
|
||||||
if (extra) event.extra = { ...event.extra, ...extra };
|
|
||||||
if (tags) event.tags = { ...event.tags, ...tags };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
originalException instanceof Error &&
|
|
||||||
'cause' in originalException &&
|
|
||||||
originalException.cause instanceof Error &&
|
|
||||||
'level' in originalException.cause &&
|
|
||||||
originalException.cause.level === 'warning'
|
|
||||||
) {
|
|
||||||
// handle underlying errors propagating from dependencies like ai-assistant-sdk
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalException instanceof Error && originalException.stack) {
|
|
||||||
const eventHash = createHash('sha1').update(originalException.stack).digest('base64');
|
|
||||||
if (seenErrors.has(eventHash)) return null;
|
|
||||||
seenErrors.add(eventHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
return event;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setTag('server_type', Container.get(InstanceSettings).instanceType);
|
|
||||||
|
|
||||||
ErrorReporterProxy.init({
|
|
||||||
report: (error, options) => captureException(error, options),
|
|
||||||
});
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
};
|
|
|
@ -1,9 +1,5 @@
|
||||||
import {
|
import { ErrorReporter } from 'n8n-core';
|
||||||
ErrorReporterProxy,
|
import type { IRunExecutionData, ITaskData, IWorkflowBase } from 'n8n-workflow';
|
||||||
type IRunExecutionData,
|
|
||||||
type ITaskData,
|
|
||||||
type IWorkflowBase,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import { saveExecutionProgress } from '@/execution-lifecycle-hooks/save-execution-progress';
|
import { saveExecutionProgress } from '@/execution-lifecycle-hooks/save-execution-progress';
|
||||||
|
@ -13,7 +9,7 @@ import { Logger } from '@/logging/logger.service';
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
|
|
||||||
mockInstance(Logger);
|
mockInstance(Logger);
|
||||||
|
const errorReporter = mockInstance(ErrorReporter);
|
||||||
const executionRepository = mockInstance(ExecutionRepository);
|
const executionRepository = mockInstance(ExecutionRepository);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -63,8 +59,6 @@ test('should update execution when saving progress is enabled', async () => {
|
||||||
progress: true,
|
progress: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const reporterSpy = jest.spyOn(ErrorReporterProxy, 'error');
|
|
||||||
|
|
||||||
executionRepository.findSingleExecution.mockResolvedValue({} as IExecutionResponse);
|
executionRepository.findSingleExecution.mockResolvedValue({} as IExecutionResponse);
|
||||||
|
|
||||||
await saveExecutionProgress(...commonArgs);
|
await saveExecutionProgress(...commonArgs);
|
||||||
|
@ -83,7 +77,7 @@ test('should update execution when saving progress is enabled', async () => {
|
||||||
status: 'running',
|
status: 'running',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(reporterSpy).not.toHaveBeenCalled();
|
expect(errorReporter.error).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should report error on failure', async () => {
|
test('should report error on failure', async () => {
|
||||||
|
@ -92,8 +86,6 @@ test('should report error on failure', async () => {
|
||||||
progress: true,
|
progress: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const reporterSpy = jest.spyOn(ErrorReporterProxy, 'error');
|
|
||||||
|
|
||||||
const error = new Error('Something went wrong');
|
const error = new Error('Something went wrong');
|
||||||
|
|
||||||
executionRepository.findSingleExecution.mockImplementation(() => {
|
executionRepository.findSingleExecution.mockImplementation(() => {
|
||||||
|
@ -103,5 +95,5 @@ test('should report error on failure', async () => {
|
||||||
await saveExecutionProgress(...commonArgs);
|
await saveExecutionProgress(...commonArgs);
|
||||||
|
|
||||||
expect(executionRepository.updateExistingExecution).not.toHaveBeenCalled();
|
expect(executionRepository.updateExistingExecution).not.toHaveBeenCalled();
|
||||||
expect(reporterSpy).toHaveBeenCalledWith(error);
|
expect(errorReporter.error).toHaveBeenCalledWith(error);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { ErrorReporter } from 'n8n-core';
|
||||||
import type { IRunExecutionData, ITaskData, IWorkflowBase } from 'n8n-workflow';
|
import type { IRunExecutionData, ITaskData, IWorkflowBase } from 'n8n-workflow';
|
||||||
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
|
@ -85,7 +85,7 @@ export async function saveExecutionProgress(
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e instanceof Error ? e : new Error(`${e}`);
|
const error = e instanceof Error ? e : new Error(`${e}`);
|
||||||
|
|
||||||
ErrorReporter.error(error);
|
Container.get(ErrorReporter).error(error);
|
||||||
// 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.
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { ErrorReporterProxy, ExpressionEvaluatorProxy } from 'n8n-workflow';
|
import { ErrorReporter } from 'n8n-core';
|
||||||
|
import { ExpressionEvaluatorProxy } from 'n8n-workflow';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
|
@ -6,7 +8,7 @@ export const initExpressionEvaluator = () => {
|
||||||
ExpressionEvaluatorProxy.setEvaluator(config.getEnv('expression.evaluator'));
|
ExpressionEvaluatorProxy.setEvaluator(config.getEnv('expression.evaluator'));
|
||||||
ExpressionEvaluatorProxy.setDifferEnabled(config.getEnv('expression.reportDifference'));
|
ExpressionEvaluatorProxy.setDifferEnabled(config.getEnv('expression.reportDifference'));
|
||||||
ExpressionEvaluatorProxy.setDiffReporter((expr) => {
|
ExpressionEvaluatorProxy.setDiffReporter((expr) => {
|
||||||
ErrorReporterProxy.warn('Expression difference', {
|
Container.get(ErrorReporter).warn('Expression difference', {
|
||||||
extra: {
|
extra: {
|
||||||
expression: expr,
|
expression: expr,
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,7 @@ import fsPromises from 'fs/promises';
|
||||||
import type { Class, DirectoryLoader, Types } from 'n8n-core';
|
import type { Class, DirectoryLoader, Types } from 'n8n-core';
|
||||||
import {
|
import {
|
||||||
CUSTOM_EXTENSION_ENV,
|
CUSTOM_EXTENSION_ENV,
|
||||||
|
ErrorReporter,
|
||||||
InstanceSettings,
|
InstanceSettings,
|
||||||
CustomDirectoryLoader,
|
CustomDirectoryLoader,
|
||||||
PackageDirectoryLoader,
|
PackageDirectoryLoader,
|
||||||
|
@ -22,7 +23,7 @@ import type {
|
||||||
INodeType,
|
INodeType,
|
||||||
IVersionedNodeType,
|
IVersionedNodeType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeHelpers, ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
import { NodeHelpers, ApplicationError } from 'n8n-workflow';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import picocolors from 'picocolors';
|
import picocolors from 'picocolors';
|
||||||
import { Container, Service } from 'typedi';
|
import { Container, Service } from 'typedi';
|
||||||
|
@ -63,6 +64,7 @@ export class LoadNodesAndCredentials {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly instanceSettings: InstanceSettings,
|
private readonly instanceSettings: InstanceSettings,
|
||||||
private readonly globalConfig: GlobalConfig,
|
private readonly globalConfig: GlobalConfig,
|
||||||
) {}
|
) {}
|
||||||
|
@ -155,7 +157,7 @@ export class LoadNodesAndCredentials {
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error((error as Error).message);
|
this.logger.error((error as Error).message);
|
||||||
ErrorReporter.error(error);
|
this.errorReporter.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { PushPayload, PushType } from '@n8n/api-types';
|
import type { PushPayload, PushType } from '@n8n/api-types';
|
||||||
|
import { ErrorReporter } from 'n8n-core';
|
||||||
import { assert, jsonStringify } from 'n8n-workflow';
|
import { assert, jsonStringify } from 'n8n-workflow';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
@ -27,7 +28,10 @@ export abstract class AbstractPush<Connection> extends TypedEmitter<AbstractPush
|
||||||
protected abstract sendToOneConnection(connection: Connection, data: string): void;
|
protected abstract sendToOneConnection(connection: Connection, data: string): void;
|
||||||
protected abstract ping(connection: Connection): void;
|
protected abstract ping(connection: Connection): void;
|
||||||
|
|
||||||
constructor(protected readonly logger: Logger) {
|
constructor(
|
||||||
|
protected readonly logger: Logger,
|
||||||
|
protected readonly errorReporter: ErrorReporter,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
// Ping all connected clients every 60 seconds
|
// Ping all connected clients every 60 seconds
|
||||||
setInterval(() => this.pingAll(), 60 * 1000);
|
setInterval(() => this.pingAll(), 60 * 1000);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow';
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import type WebSocket from 'ws';
|
import type WebSocket from 'ws';
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export class WebSocketPush extends AbstractPush<WebSocket> {
|
||||||
|
|
||||||
this.onMessageReceived(pushRef, JSON.parse(buffer.toString('utf8')));
|
this.onMessageReceived(pushRef, JSON.parse(buffer.toString('utf8')));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporterProxy.error(
|
this.errorReporter.error(
|
||||||
new ApplicationError('Error parsing push message', {
|
new ApplicationError('Error parsing push message', {
|
||||||
extra: {
|
extra: {
|
||||||
userId,
|
userId,
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import {
|
import { ErrorReporter } from 'n8n-core';
|
||||||
ErrorReporterProxy as ErrorReporter,
|
import { FORM_TRIGGER_PATH_IDENTIFIER, NodeApiError } from 'n8n-workflow';
|
||||||
FORM_TRIGGER_PATH_IDENTIFIER,
|
|
||||||
NodeApiError,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import picocolors from 'picocolors';
|
import picocolors from 'picocolors';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
|
@ -141,7 +138,7 @@ export const isUniqueConstraintError = (error: Error) =>
|
||||||
|
|
||||||
export function reportError(error: Error) {
|
export function reportError(error: Error) {
|
||||||
if (!(error instanceof ResponseError) || error.httpStatusCode > 404) {
|
if (!(error instanceof ResponseError) || error.httpStatusCode > 404) {
|
||||||
ErrorReporter.error(error);
|
Container.get(ErrorReporter).error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { TaskRunnersConfig } from '@n8n/config';
|
import { TaskRunnersConfig } from '@n8n/config';
|
||||||
import { ErrorReporterProxy, sleep } from 'n8n-workflow';
|
import { ErrorReporter } from 'n8n-core';
|
||||||
|
import { sleep } from 'n8n-workflow';
|
||||||
import * as a from 'node:assert/strict';
|
import * as a from 'node:assert/strict';
|
||||||
import Container, { Service } from 'typedi';
|
import Container, { Service } from 'typedi';
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ export class TaskRunnerModule {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly runnerConfig: TaskRunnersConfig,
|
private readonly runnerConfig: TaskRunnersConfig,
|
||||||
) {
|
) {
|
||||||
this.logger = this.logger.scoped('task-runner');
|
this.logger = this.logger.scoped('task-runner');
|
||||||
|
@ -114,7 +116,7 @@ export class TaskRunnerModule {
|
||||||
|
|
||||||
private onRunnerRestartLoopDetected = async (error: TaskRunnerRestartLoopError) => {
|
private onRunnerRestartLoopDetected = async (error: TaskRunnerRestartLoopError) => {
|
||||||
this.logger.error(error.message);
|
this.logger.error(error.message);
|
||||||
ErrorReporterProxy.error(error);
|
this.errorReporter.error(error);
|
||||||
|
|
||||||
// Allow some time for the error to be flushed
|
// Allow some time for the error to be flushed
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
|
|
@ -12,7 +12,14 @@ describe('JobProcessor', () => {
|
||||||
executionRepository.findSingleExecution.mockResolvedValue(
|
executionRepository.findSingleExecution.mockResolvedValue(
|
||||||
mock<IExecutionResponse>({ status: 'crashed' }),
|
mock<IExecutionResponse>({ status: 'crashed' }),
|
||||||
);
|
);
|
||||||
const jobProcessor = new JobProcessor(mock(), executionRepository, mock(), mock(), mock());
|
const jobProcessor = new JobProcessor(
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
executionRepository,
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await jobProcessor.processJob(mock<Job>());
|
const result = await jobProcessor.processJob(mock<Job>());
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,7 @@ describe('ScalingService', () => {
|
||||||
scalingService = new ScalingService(
|
scalingService = new ScalingService(
|
||||||
mockLogger(),
|
mockLogger(),
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
jobProcessor,
|
jobProcessor,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
mock(),
|
mock(),
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import type { RunningJobSummary } from '@n8n/api-types';
|
import type { RunningJobSummary } from '@n8n/api-types';
|
||||||
import { InstanceSettings, WorkflowExecute } from 'n8n-core';
|
import { ErrorReporter, InstanceSettings, WorkflowExecute } from 'n8n-core';
|
||||||
import type { ExecutionStatus, IExecuteResponsePromiseData, IRun } from 'n8n-workflow';
|
import type { ExecutionStatus, IExecuteResponsePromiseData, IRun } from 'n8n-workflow';
|
||||||
import {
|
import { BINARY_ENCODING, ApplicationError, Workflow } from 'n8n-workflow';
|
||||||
BINARY_ENCODING,
|
|
||||||
ApplicationError,
|
|
||||||
Workflow,
|
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import type PCancelable from 'p-cancelable';
|
import type PCancelable from 'p-cancelable';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
@ -35,6 +30,7 @@ export class JobProcessor {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly executionRepository: ExecutionRepository,
|
private readonly executionRepository: ExecutionRepository,
|
||||||
private readonly workflowRepository: WorkflowRepository,
|
private readonly workflowRepository: WorkflowRepository,
|
||||||
private readonly nodeTypes: NodeTypes,
|
private readonly nodeTypes: NodeTypes,
|
||||||
|
@ -155,7 +151,7 @@ export class JobProcessor {
|
||||||
workflowExecute = new WorkflowExecute(additionalData, execution.mode, execution.data);
|
workflowExecute = new WorkflowExecute(additionalData, execution.mode, execution.data);
|
||||||
workflowRun = workflowExecute.processRunExecutionData(workflow);
|
workflowRun = workflowExecute.processRunExecutionData(workflow);
|
||||||
} else {
|
} else {
|
||||||
ErrorReporter.info(`Worker found execution ${executionId} without data`);
|
this.errorReporter.info(`Worker found execution ${executionId} without data`);
|
||||||
// Execute all nodes
|
// Execute all nodes
|
||||||
// Can execute without webhook so go on
|
// Can execute without webhook so go on
|
||||||
workflowExecute = new WorkflowExecute(additionalData, execution.mode);
|
workflowExecute = new WorkflowExecute(additionalData, execution.mode);
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { InstanceSettings } from 'n8n-core';
|
import { ErrorReporter, InstanceSettings } from 'n8n-core';
|
||||||
import {
|
import { ApplicationError, BINARY_ENCODING, sleep, jsonStringify, ensureError } from 'n8n-workflow';
|
||||||
ApplicationError,
|
|
||||||
BINARY_ENCODING,
|
|
||||||
sleep,
|
|
||||||
jsonStringify,
|
|
||||||
ErrorReporterProxy,
|
|
||||||
ensureError,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import type { IExecuteResponsePromiseData } from 'n8n-workflow';
|
import type { IExecuteResponsePromiseData } from 'n8n-workflow';
|
||||||
import { strict } from 'node:assert';
|
import { strict } from 'node:assert';
|
||||||
import Container, { Service } from 'typedi';
|
import Container, { Service } from 'typedi';
|
||||||
|
@ -43,6 +36,7 @@ export class ScalingService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly activeExecutions: ActiveExecutions,
|
private readonly activeExecutions: ActiveExecutions,
|
||||||
private readonly jobProcessor: JobProcessor,
|
private readonly jobProcessor: JobProcessor,
|
||||||
private readonly globalConfig: GlobalConfig,
|
private readonly globalConfig: GlobalConfig,
|
||||||
|
@ -119,7 +113,7 @@ export class ScalingService {
|
||||||
|
|
||||||
await job.progress(msg);
|
await job.progress(msg);
|
||||||
|
|
||||||
ErrorReporterProxy.error(error, { executionId });
|
this.errorReporter.error(error, { executionId });
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,13 @@ import { CredentialsTester } from '@/services/credentials-tester.service';
|
||||||
describe('CredentialsTester', () => {
|
describe('CredentialsTester', () => {
|
||||||
const credentialTypes = mock<CredentialTypes>();
|
const credentialTypes = mock<CredentialTypes>();
|
||||||
const nodeTypes = mock<NodeTypes>();
|
const nodeTypes = mock<NodeTypes>();
|
||||||
const credentialsTester = new CredentialsTester(mock(), credentialTypes, nodeTypes, mock());
|
const credentialsTester = new CredentialsTester(
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
credentialTypes,
|
||||||
|
nodeTypes,
|
||||||
|
mock(),
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { NodeExecuteFunctions } from 'n8n-core';
|
import { ErrorReporter, NodeExecuteFunctions } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
ICredentialsDecrypted,
|
ICredentialsDecrypted,
|
||||||
ICredentialTestFunction,
|
ICredentialTestFunction,
|
||||||
|
@ -28,7 +28,6 @@ import {
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
RoutingNode,
|
RoutingNode,
|
||||||
Workflow,
|
Workflow,
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
@ -75,6 +74,7 @@ const mockNodeTypes: INodeTypes = {
|
||||||
export class CredentialsTester {
|
export class CredentialsTester {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly credentialTypes: CredentialTypes,
|
private readonly credentialTypes: CredentialTypes,
|
||||||
private readonly nodeTypes: NodeTypes,
|
private readonly nodeTypes: NodeTypes,
|
||||||
private readonly credentialsHelper: CredentialsHelper,
|
private readonly credentialsHelper: CredentialsHelper,
|
||||||
|
@ -316,7 +316,7 @@ export class CredentialsTester {
|
||||||
credentialsDecrypted,
|
credentialsDecrypted,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
this.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) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow';
|
import type { ErrorReporter } from 'n8n-core';
|
||||||
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
|
|
||||||
import type { ServiceClass } from '@/shutdown/shutdown.service';
|
import type { ServiceClass } from '@/shutdown/shutdown.service';
|
||||||
|
@ -13,14 +14,13 @@ describe('ShutdownService', () => {
|
||||||
let shutdownService: ShutdownService;
|
let shutdownService: ShutdownService;
|
||||||
let mockComponent: MockComponent;
|
let mockComponent: MockComponent;
|
||||||
let onShutdownSpy: jest.SpyInstance;
|
let onShutdownSpy: jest.SpyInstance;
|
||||||
let mockErrorReporterProxy: jest.SpyInstance;
|
const errorReporter = mock<ErrorReporter>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
shutdownService = new ShutdownService(mock());
|
shutdownService = new ShutdownService(mock(), errorReporter);
|
||||||
mockComponent = new MockComponent();
|
mockComponent = new MockComponent();
|
||||||
Container.set(MockComponent, mockComponent);
|
Container.set(MockComponent, mockComponent);
|
||||||
onShutdownSpy = jest.spyOn(mockComponent, 'onShutdown');
|
onShutdownSpy = jest.spyOn(mockComponent, 'onShutdown');
|
||||||
mockErrorReporterProxy = jest.spyOn(ErrorReporterProxy, 'error').mockImplementation(() => {});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('shutdown', () => {
|
describe('shutdown', () => {
|
||||||
|
@ -83,8 +83,8 @@ describe('ShutdownService', () => {
|
||||||
shutdownService.shutdown();
|
shutdownService.shutdown();
|
||||||
await shutdownService.waitForShutdown();
|
await shutdownService.waitForShutdown();
|
||||||
|
|
||||||
expect(mockErrorReporterProxy).toHaveBeenCalledTimes(1);
|
expect(errorReporter.error).toHaveBeenCalledTimes(1);
|
||||||
const error = mockErrorReporterProxy.mock.calls[0][0];
|
const error = errorReporter.error.mock.calls[0][0] as ApplicationError;
|
||||||
expect(error).toBeInstanceOf(ApplicationError);
|
expect(error).toBeInstanceOf(ApplicationError);
|
||||||
expect(error.message).toBe('Failed to shutdown gracefully');
|
expect(error.message).toBe('Failed to shutdown gracefully');
|
||||||
expect(error.extra).toEqual({
|
expect(error.extra).toEqual({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Class } from 'n8n-core';
|
import { type Class, ErrorReporter } from 'n8n-core';
|
||||||
import { ApplicationError, ErrorReporterProxy, assert } from 'n8n-workflow';
|
import { ApplicationError, assert } from 'n8n-workflow';
|
||||||
import { Container, Service } from 'typedi';
|
import { Container, Service } from 'typedi';
|
||||||
|
|
||||||
import { LOWEST_SHUTDOWN_PRIORITY, HIGHEST_SHUTDOWN_PRIORITY } from '@/constants';
|
import { LOWEST_SHUTDOWN_PRIORITY, HIGHEST_SHUTDOWN_PRIORITY } from '@/constants';
|
||||||
|
@ -31,7 +31,10 @@ export class ShutdownService {
|
||||||
|
|
||||||
private shutdownPromise: Promise<void> | undefined;
|
private shutdownPromise: Promise<void> | undefined;
|
||||||
|
|
||||||
constructor(private readonly logger: Logger) {}
|
constructor(
|
||||||
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** Registers given listener to be notified when the application is shutting down */
|
/** Registers given listener to be notified when the application is shutting down */
|
||||||
register(priority: number, handler: ShutdownHandler) {
|
register(priority: number, handler: ShutdownHandler) {
|
||||||
|
@ -108,7 +111,7 @@ export class ShutdownService {
|
||||||
await method.call(service);
|
await method.call(service);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(error instanceof Error);
|
assert(error instanceof Error);
|
||||||
ErrorReporterProxy.error(new ComponentShutdownError(name, error));
|
this.errorReporter.error(new ComponentShutdownError(name, error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
import { ErrorReporter } from 'n8n-core';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import type { Transporter } from 'nodemailer';
|
import type { Transporter } from 'nodemailer';
|
||||||
import { createTransport } from 'nodemailer';
|
import { createTransport } from 'nodemailer';
|
||||||
|
@ -20,6 +20,7 @@ export class NodeMailer {
|
||||||
constructor(
|
constructor(
|
||||||
globalConfig: GlobalConfig,
|
globalConfig: GlobalConfig,
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
) {
|
) {
|
||||||
const smtpConfig = globalConfig.userManagement.emails.smtp;
|
const smtpConfig = globalConfig.userManagement.emails.smtp;
|
||||||
const transportConfig: SMTPConnection.Options = pick(smtpConfig, ['host', 'port', 'secure']);
|
const transportConfig: SMTPConnection.Options = pick(smtpConfig, ['host', 'port', 'secure']);
|
||||||
|
@ -66,7 +67,7 @@ export class NodeMailer {
|
||||||
`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);
|
this.errorReporter.error(error);
|
||||||
this.logger.error('Failed to send email', {
|
this.logger.error('Failed to send email', {
|
||||||
recipients: mailData.emailRecipients,
|
recipients: mailData.emailRecipients,
|
||||||
error: error as Error,
|
error: error as Error,
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { BinaryDataService, NodeExecuteFunctions } from 'n8n-core';
|
import { BinaryDataService, ErrorReporter, NodeExecuteFunctions } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
IBinaryData,
|
IBinaryData,
|
||||||
IBinaryKeyData,
|
IBinaryKeyData,
|
||||||
|
@ -33,8 +33,6 @@ import {
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
BINARY_ENCODING,
|
BINARY_ENCODING,
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
ErrorReporterProxy,
|
|
||||||
ExecutionCancelledError,
|
ExecutionCancelledError,
|
||||||
FORM_NODE_TYPE,
|
FORM_NODE_TYPE,
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
|
@ -280,7 +278,7 @@ export async function executeWebhook(
|
||||||
errorMessage = err.message;
|
errorMessage = err.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorReporterProxy.error(err, {
|
Container.get(ErrorReporter).error(err, {
|
||||||
extra: {
|
extra: {
|
||||||
nodeName: workflowStartNode.name,
|
nodeName: workflowStartNode.name,
|
||||||
nodeType: workflowStartNode.type,
|
nodeType: workflowStartNode.type,
|
||||||
|
@ -521,7 +519,7 @@ export async function executeWebhook(
|
||||||
didSendResponse = true;
|
didSendResponse = true;
|
||||||
})
|
})
|
||||||
.catch(async (error) => {
|
.catch(async (error) => {
|
||||||
ErrorReporter.error(error);
|
Container.get(ErrorReporter).error(error);
|
||||||
Container.get(Logger).error(
|
Container.get(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 },
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import type { PushType } from '@n8n/api-types';
|
import type { PushType } from '@n8n/api-types';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { stringify } from 'flatted';
|
import { stringify } from 'flatted';
|
||||||
import { WorkflowExecute } from 'n8n-core';
|
import { ErrorReporter, WorkflowExecute } from 'n8n-core';
|
||||||
import {
|
import { ApplicationError, NodeOperationError, Workflow, WorkflowHooks } from 'n8n-workflow';
|
||||||
ApplicationError,
|
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
NodeOperationError,
|
|
||||||
Workflow,
|
|
||||||
WorkflowHooks,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
|
@ -215,7 +208,7 @@ export function executeErrorWorkflow(
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
ErrorReporter.error(error);
|
Container.get(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`,
|
||||||
{
|
{
|
||||||
|
@ -423,7 +416,7 @@ function hookFunctionsSave(): IWorkflowExecuteHooks {
|
||||||
newStaticData,
|
newStaticData,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorReporter.error(e);
|
Container.get(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 },
|
||||||
|
@ -502,7 +495,7 @@ function hookFunctionsSave(): IWorkflowExecuteHooks {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
Container.get(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,
|
||||||
|
@ -584,7 +577,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
newStaticData,
|
newStaticData,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorReporter.error(e);
|
Container.get(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)`,
|
||||||
{ pushRef: this.pushRef, workflowId: this.workflowData.id },
|
{ pushRef: this.pushRef, workflowId: this.workflowData.id },
|
||||||
|
@ -653,7 +646,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
this.executionId,
|
this.executionId,
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
Container.get(ErrorReporter).error(error);
|
||||||
Container.get(Logger).error(
|
Container.get(Logger).error(
|
||||||
'There was a problem running hook "workflow.postExecute"',
|
'There was a problem running hook "workflow.postExecute"',
|
||||||
error,
|
error,
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import * as a from 'assert/strict';
|
import * as a from 'assert/strict';
|
||||||
import {
|
import {
|
||||||
DirectedGraph,
|
DirectedGraph,
|
||||||
|
ErrorReporter,
|
||||||
InstanceSettings,
|
InstanceSettings,
|
||||||
WorkflowExecute,
|
WorkflowExecute,
|
||||||
filterDisabledNodes,
|
filterDisabledNodes,
|
||||||
|
@ -22,11 +23,7 @@ import type {
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import { ExecutionCancelledError, Workflow } from 'n8n-workflow';
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
ExecutionCancelledError,
|
|
||||||
Workflow,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import PCancelable from 'p-cancelable';
|
import PCancelable from 'p-cancelable';
|
||||||
import { Container, Service } from 'typedi';
|
import { Container, Service } from 'typedi';
|
||||||
|
|
||||||
|
@ -55,6 +52,7 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly activeExecutions: ActiveExecutions,
|
private readonly activeExecutions: ActiveExecutions,
|
||||||
private readonly executionRepository: ExecutionRepository,
|
private readonly executionRepository: ExecutionRepository,
|
||||||
private readonly externalHooks: ExternalHooks,
|
private readonly externalHooks: ExternalHooks,
|
||||||
|
@ -82,7 +80,7 @@ export class WorkflowRunner {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorReporter.error(error, { executionId });
|
this.errorReporter.error(error, { executionId });
|
||||||
|
|
||||||
const isQueueMode = config.getEnv('executions.mode') === 'queue';
|
const isQueueMode = config.getEnv('executions.mode') === 'queue';
|
||||||
|
|
||||||
|
@ -193,14 +191,14 @@ export class WorkflowRunner {
|
||||||
executionId,
|
executionId,
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
this.errorReporter.error(error);
|
||||||
this.logger.error('There was a problem running hook "workflow.postExecute"', error);
|
this.logger.error('There was a problem running hook "workflow.postExecute"', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error instanceof ExecutionCancelledError) return;
|
if (error instanceof ExecutionCancelledError) return;
|
||||||
ErrorReporter.error(error);
|
this.errorReporter.error(error);
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'There was a problem running internal hook "onWorkflowPostExecute"',
|
'There was a problem running internal hook "onWorkflowPostExecute"',
|
||||||
error,
|
error,
|
||||||
|
|
|
@ -57,6 +57,7 @@ describe('WorkflowExecutionService', () => {
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
workflowRunner,
|
workflowRunner,
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
|
import { ErrorReporter } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
IDeferredPromise,
|
IDeferredPromise,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
|
@ -11,11 +12,7 @@ import type {
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import { SubworkflowOperationError, Workflow } from 'n8n-workflow';
|
||||||
SubworkflowOperationError,
|
|
||||||
Workflow,
|
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
import type { Project } from '@/databases/entities/project';
|
import type { Project } from '@/databases/entities/project';
|
||||||
|
@ -36,6 +33,7 @@ import type { WorkflowRequest } from '@/workflows/workflow.request';
|
||||||
export class WorkflowExecutionService {
|
export class WorkflowExecutionService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly executionRepository: ExecutionRepository,
|
private readonly executionRepository: ExecutionRepository,
|
||||||
private readonly workflowRepository: WorkflowRepository,
|
private readonly workflowRepository: WorkflowRepository,
|
||||||
private readonly nodeTypes: NodeTypes,
|
private readonly nodeTypes: NodeTypes,
|
||||||
|
@ -293,7 +291,7 @@ export class WorkflowExecutionService {
|
||||||
|
|
||||||
await this.workflowRunner.run(runData);
|
await this.workflowRunner.run(runData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
this.errorReporter.error(error);
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`,
|
`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { type IDataObject, type Workflow, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
import { ErrorReporter } from 'n8n-core';
|
||||||
|
import type { IDataObject, Workflow } from 'n8n-workflow';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
|
@ -11,6 +12,7 @@ export class WorkflowStaticDataService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly globalConfig: GlobalConfig,
|
private readonly globalConfig: GlobalConfig,
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly workflowRepository: WorkflowRepository,
|
private readonly workflowRepository: WorkflowRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -33,7 +35,7 @@ export class WorkflowStaticDataService {
|
||||||
await this.saveStaticDataById(workflow.id, workflow.staticData);
|
await this.saveStaticDataById(workflow.id, workflow.staticData);
|
||||||
workflow.staticData.__dataChanged = false;
|
workflow.staticData.__dataChanged = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
this.errorReporter.error(error);
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
`There was a problem saving the workflow with id "${workflow.id}" to save changed Data: "${error.message}"`,
|
`There was a problem saving the workflow with id "${workflow.id}" to save changed Data: "${error.message}"`,
|
||||||
|
|
|
@ -283,6 +283,7 @@ describe('shouldAddWebhooks', () => {
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
mock<InstanceSettings>({ isLeader: true, isFollower: false }),
|
mock<InstanceSettings>({ isLeader: true, isFollower: false }),
|
||||||
mock(),
|
mock(),
|
||||||
);
|
);
|
||||||
|
@ -322,6 +323,7 @@ describe('shouldAddWebhooks', () => {
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
mock<InstanceSettings>({ isLeader: false, isFollower: true }),
|
mock<InstanceSettings>({ isLeader: false, isFollower: true }),
|
||||||
mock(),
|
mock(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -30,6 +30,7 @@ describe('SourceControlImportService', () => {
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
mock<InstanceSettings>({ n8nFolder: '/some-path' }),
|
mock<InstanceSettings>({ n8nFolder: '/some-path' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('TaskRunnerModule in external mode', () => {
|
||||||
runnerConfig.enabled = true;
|
runnerConfig.enabled = true;
|
||||||
runnerConfig.authToken = '';
|
runnerConfig.authToken = '';
|
||||||
|
|
||||||
const module = new TaskRunnerModule(mock(), runnerConfig);
|
const module = new TaskRunnerModule(mock(), mock(), runnerConfig);
|
||||||
|
|
||||||
await expect(module.start()).rejects.toThrowError(MissingAuthTokenError);
|
await expect(module.start()).rejects.toThrowError(MissingAuthTokenError);
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
"@langchain/core": "catalog:",
|
"@langchain/core": "catalog:",
|
||||||
"@n8n/client-oauth2": "workspace:*",
|
"@n8n/client-oauth2": "workspace:*",
|
||||||
"@n8n/config": "workspace:*",
|
"@n8n/config": "workspace:*",
|
||||||
|
"@sentry/node": "catalog:",
|
||||||
"aws4": "1.11.0",
|
"aws4": "1.11.0",
|
||||||
"axios": "catalog:",
|
"axios": "catalog:",
|
||||||
"concat-stream": "2.0.0",
|
"concat-stream": "2.0.0",
|
||||||
|
|
|
@ -11,7 +11,6 @@ import type {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
LoggerProxy as Logger,
|
LoggerProxy as Logger,
|
||||||
toCronExpression,
|
toCronExpression,
|
||||||
TriggerCloseError,
|
TriggerCloseError,
|
||||||
|
@ -20,12 +19,16 @@ import {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
import { ErrorReporter } from './error-reporter';
|
||||||
import type { IWorkflowData } from './Interfaces';
|
import type { IWorkflowData } from './Interfaces';
|
||||||
import { ScheduledTaskManager } from './ScheduledTaskManager';
|
import { ScheduledTaskManager } from './ScheduledTaskManager';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ActiveWorkflows {
|
export class ActiveWorkflows {
|
||||||
constructor(private readonly scheduledTaskManager: ScheduledTaskManager) {}
|
constructor(
|
||||||
|
private readonly scheduledTaskManager: ScheduledTaskManager,
|
||||||
|
private readonly errorReporter: ErrorReporter,
|
||||||
|
) {}
|
||||||
|
|
||||||
private activeWorkflows: { [workflowId: string]: IWorkflowData } = {};
|
private activeWorkflows: { [workflowId: string]: IWorkflowData } = {};
|
||||||
|
|
||||||
|
@ -218,7 +221,7 @@ export class ActiveWorkflows {
|
||||||
Logger.error(
|
Logger.error(
|
||||||
`There was a problem calling "closeFunction" on "${e.node.name}" in workflow "${workflowId}"`,
|
`There was a problem calling "closeFunction" on "${e.node.name}" in workflow "${workflowId}"`,
|
||||||
);
|
);
|
||||||
ErrorReporter.error(e, { extra: { workflowId } });
|
this.errorReporter.error(e, { extra: { workflowId } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,11 +46,12 @@ import {
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
NodeExecutionOutput,
|
NodeExecutionOutput,
|
||||||
sleep,
|
sleep,
|
||||||
ErrorReporterProxy,
|
|
||||||
ExecutionCancelledError,
|
ExecutionCancelledError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import PCancelable from 'p-cancelable';
|
import PCancelable from 'p-cancelable';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
|
import { ErrorReporter } from './error-reporter';
|
||||||
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
||||||
import {
|
import {
|
||||||
DirectedGraph,
|
DirectedGraph,
|
||||||
|
@ -1428,7 +1429,7 @@ export class WorkflowExecute {
|
||||||
toReport = error;
|
toReport = error;
|
||||||
}
|
}
|
||||||
if (toReport) {
|
if (toReport) {
|
||||||
ErrorReporterProxy.error(toReport, {
|
Container.get(ErrorReporter).error(toReport, {
|
||||||
extra: {
|
extra: {
|
||||||
nodeName: executionNode.name,
|
nodeName: executionNode.name,
|
||||||
nodeType: executionNode.type,
|
nodeType: executionNode.type,
|
||||||
|
|
171
packages/core/src/error-reporter.ts
Normal file
171
packages/core/src/error-reporter.ts
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import { GlobalConfig } from '@n8n/config';
|
||||||
|
import type { NodeOptions } from '@sentry/node';
|
||||||
|
import type { ErrorEvent, EventHint } from '@sentry/types';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ApplicationError, LoggerProxy, type ReportingOptions } from 'n8n-workflow';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
import { InstanceSettings } from './InstanceSettings';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class ErrorReporter {
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
/** Hashes of error stack traces, to deduplicate error reports. */
|
||||||
|
private seenErrors = new Set<string>();
|
||||||
|
|
||||||
|
private report: (error: Error | string, options?: ReportingOptions) => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly globalConfig: GlobalConfig,
|
||||||
|
private readonly instanceSettings: InstanceSettings,
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
this.report = this.defaultReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
private defaultReport(error: Error | string, options?: ReportingOptions) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
let e = error;
|
||||||
|
|
||||||
|
const { executionId } = options ?? {};
|
||||||
|
const context = executionId ? ` (execution ${executionId})` : '';
|
||||||
|
|
||||||
|
do {
|
||||||
|
const msg = [e.message + context, e.stack ? `\n${e.stack}\n` : ''].join('');
|
||||||
|
const meta = e instanceof ApplicationError ? e.extra : undefined;
|
||||||
|
LoggerProxy.error(msg, meta);
|
||||||
|
e = e.cause as Error;
|
||||||
|
} while (e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
this.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dsn = this.globalConfig.sentry.backendDsn;
|
||||||
|
if (!dsn) {
|
||||||
|
this.initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect longer stacktraces
|
||||||
|
Error.stackTraceLimit = 50;
|
||||||
|
|
||||||
|
const {
|
||||||
|
N8N_VERSION: release,
|
||||||
|
ENVIRONMENT: environment,
|
||||||
|
DEPLOYMENT_NAME: serverName,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
const { init, captureException, setTag } = await import('@sentry/node');
|
||||||
|
const { requestDataIntegration, rewriteFramesIntegration } = await import('@sentry/node');
|
||||||
|
|
||||||
|
const enabledIntegrations = [
|
||||||
|
'InboundFilters',
|
||||||
|
'FunctionToString',
|
||||||
|
'LinkedErrors',
|
||||||
|
'OnUnhandledRejection',
|
||||||
|
'ContextLines',
|
||||||
|
];
|
||||||
|
|
||||||
|
init({
|
||||||
|
dsn,
|
||||||
|
release,
|
||||||
|
environment,
|
||||||
|
enableTracing: false,
|
||||||
|
serverName,
|
||||||
|
beforeBreadcrumb: () => null,
|
||||||
|
beforeSend: this.beforeSend.bind(this) as NodeOptions['beforeSend'],
|
||||||
|
integrations: (integrations) => [
|
||||||
|
...integrations.filter(({ name }) => enabledIntegrations.includes(name)),
|
||||||
|
rewriteFramesIntegration({ root: process.cwd() }),
|
||||||
|
requestDataIntegration({
|
||||||
|
include: {
|
||||||
|
cookies: false,
|
||||||
|
data: false,
|
||||||
|
headers: false,
|
||||||
|
query_string: false,
|
||||||
|
url: true,
|
||||||
|
user: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
setTag('server_type', this.instanceSettings.instanceType);
|
||||||
|
|
||||||
|
this.report = (error, options) => captureException(error, options);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeSend(event: ErrorEvent, { originalException }: EventHint) {
|
||||||
|
if (!originalException) return null;
|
||||||
|
|
||||||
|
if (originalException instanceof Promise) {
|
||||||
|
originalException = await originalException.catch((error) => error as Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalException instanceof AxiosError) return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
originalException instanceof Error &&
|
||||||
|
originalException.name === 'QueryFailedError' &&
|
||||||
|
['SQLITE_FULL', 'SQLITE_IOERR'].some((errMsg) => originalException.message.includes(errMsg))
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalException instanceof ApplicationError) {
|
||||||
|
const { level, extra, tags } = originalException;
|
||||||
|
if (level === 'warning') return null;
|
||||||
|
event.level = level;
|
||||||
|
if (extra) event.extra = { ...event.extra, ...extra };
|
||||||
|
if (tags) event.tags = { ...event.tags, ...tags };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
originalException instanceof Error &&
|
||||||
|
'cause' in originalException &&
|
||||||
|
originalException.cause instanceof Error &&
|
||||||
|
'level' in originalException.cause &&
|
||||||
|
originalException.cause.level === 'warning'
|
||||||
|
) {
|
||||||
|
// handle underlying errors propagating from dependencies like ai-assistant-sdk
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalException instanceof Error && originalException.stack) {
|
||||||
|
const eventHash = createHash('sha1').update(originalException.stack).digest('base64');
|
||||||
|
if (this.seenErrors.has(eventHash)) return null;
|
||||||
|
this.seenErrors.add(eventHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
error(e: unknown, options?: ReportingOptions) {
|
||||||
|
const toReport = this.wrap(e);
|
||||||
|
if (toReport) this.report(toReport, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(warning: Error | string, options?: ReportingOptions) {
|
||||||
|
this.error(warning, { ...options, level: 'warning' });
|
||||||
|
}
|
||||||
|
|
||||||
|
info(msg: string, options?: ReportingOptions) {
|
||||||
|
this.report(msg, { ...options, level: 'info' });
|
||||||
|
}
|
||||||
|
|
||||||
|
private wrap(e: unknown) {
|
||||||
|
if (e instanceof Error) return e;
|
||||||
|
if (typeof e === 'string') return new ApplicationError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,3 +21,4 @@ export { isStoredMode as isValidNonDefaultMode } from './BinaryData/utils';
|
||||||
export * from './ExecutionMetadata';
|
export * from './ExecutionMetadata';
|
||||||
export * from './node-execution-context';
|
export * from './node-execution-context';
|
||||||
export * from './PartialExecutionUtils';
|
export * from './PartialExecutionUtils';
|
||||||
|
export { ErrorReporter } from './error-reporter';
|
||||||
|
|
110
packages/core/test/error-reporter.test.ts
Normal file
110
packages/core/test/error-reporter.test.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import type { GlobalConfig } from '@n8n/config';
|
||||||
|
import { QueryFailedError } from '@n8n/typeorm';
|
||||||
|
import type { ErrorEvent } from '@sentry/types';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { ErrorReporter } from '@/error-reporter';
|
||||||
|
import type { InstanceSettings } from '@/InstanceSettings';
|
||||||
|
|
||||||
|
const init = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('@sentry/node', () => ({
|
||||||
|
init,
|
||||||
|
setTag: jest.fn(),
|
||||||
|
captureException: jest.fn(),
|
||||||
|
Integrations: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.spyOn(process, 'on');
|
||||||
|
|
||||||
|
describe('ErrorReporter', () => {
|
||||||
|
const globalConfig = mock<GlobalConfig>();
|
||||||
|
const instanceSettings = mock<InstanceSettings>();
|
||||||
|
const errorReporting = new ErrorReporter(globalConfig, instanceSettings);
|
||||||
|
const event = {} as ErrorEvent;
|
||||||
|
|
||||||
|
describe('beforeSend', () => {
|
||||||
|
it('should ignore errors with level warning', async () => {
|
||||||
|
const originalException = new ApplicationError('test');
|
||||||
|
originalException.level = 'warning';
|
||||||
|
|
||||||
|
expect(await errorReporting.beforeSend(event, { originalException })).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep events with a cause with error level', async () => {
|
||||||
|
const cause = new Error('cause-error');
|
||||||
|
const originalException = new ApplicationError('test', cause);
|
||||||
|
|
||||||
|
expect(await errorReporting.beforeSend(event, { originalException })).toEqual(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore events with error cause with warning level', async () => {
|
||||||
|
const cause: Error & { level?: 'warning' } = new Error('cause-error');
|
||||||
|
cause.level = 'warning';
|
||||||
|
const originalException = new ApplicationError('test', cause);
|
||||||
|
|
||||||
|
expect(await errorReporting.beforeSend(event, { originalException })).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set level, extra, and tags from ApplicationError', async () => {
|
||||||
|
const originalException = new ApplicationError('Test error', {
|
||||||
|
level: 'error',
|
||||||
|
extra: { foo: 'bar' },
|
||||||
|
tags: { tag1: 'value1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const testEvent = {} as ErrorEvent;
|
||||||
|
|
||||||
|
const result = await errorReporting.beforeSend(testEvent, { originalException });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
level: 'error',
|
||||||
|
extra: { foo: 'bar' },
|
||||||
|
tags: { tag1: 'value1' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deduplicate errors with same stack trace', async () => {
|
||||||
|
const originalException = new Error();
|
||||||
|
|
||||||
|
const firstResult = await errorReporting.beforeSend(event, { originalException });
|
||||||
|
expect(firstResult).toEqual(event);
|
||||||
|
|
||||||
|
const secondResult = await errorReporting.beforeSend(event, { originalException });
|
||||||
|
expect(secondResult).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Promise rejections', async () => {
|
||||||
|
const originalException = Promise.reject(new Error());
|
||||||
|
|
||||||
|
const result = await errorReporting.beforeSend(event, { originalException });
|
||||||
|
|
||||||
|
expect(result).toEqual(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['undefined', undefined],
|
||||||
|
['null', null],
|
||||||
|
['an AxiosError', new AxiosError()],
|
||||||
|
['a rejected Promise with AxiosError', Promise.reject(new AxiosError())],
|
||||||
|
[
|
||||||
|
'a QueryFailedError with SQLITE_FULL',
|
||||||
|
new QueryFailedError('', [], new Error('SQLITE_FULL')),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'a QueryFailedError with SQLITE_IOERR',
|
||||||
|
new QueryFailedError('', [], new Error('SQLITE_IOERR')),
|
||||||
|
],
|
||||||
|
['an ApplicationError with "warning" level', new ApplicationError('', { level: 'warning' })],
|
||||||
|
[
|
||||||
|
'an Error with ApplicationError as cause with "warning" level',
|
||||||
|
new Error('', { cause: new ApplicationError('', { level: 'warning' }) }),
|
||||||
|
],
|
||||||
|
])('should ignore if originalException is %s', async (_, originalException) => {
|
||||||
|
const result = await errorReporting.beforeSend(event, { originalException });
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,47 +0,0 @@
|
||||||
import { ApplicationError, type ReportingOptions } from './errors/application.error';
|
|
||||||
import * as Logger from './LoggerProxy';
|
|
||||||
|
|
||||||
interface ErrorReporter {
|
|
||||||
report: (error: Error | string, options?: ReportingOptions) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance: ErrorReporter = {
|
|
||||||
report: (error, options) => {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
let e = error;
|
|
||||||
|
|
||||||
const { executionId } = options ?? {};
|
|
||||||
const context = executionId ? ` (execution ${executionId})` : '';
|
|
||||||
|
|
||||||
do {
|
|
||||||
const msg = [e.message + context, e.stack ? `\n${e.stack}\n` : ''].join('');
|
|
||||||
const meta = e instanceof ApplicationError ? e.extra : undefined;
|
|
||||||
Logger.error(msg, meta);
|
|
||||||
e = e.cause as Error;
|
|
||||||
} while (e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
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 ApplicationError(e);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const error = (e: unknown, options?: ReportingOptions) => {
|
|
||||||
const toReport = wrap(e);
|
|
||||||
if (toReport) instance.report(toReport, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const info = (msg: string, options?: ReportingOptions) => {
|
|
||||||
Logger.info(msg);
|
|
||||||
instance.report(msg, { ...options, level: 'info' });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const warn = (warning: Error | string, options?: ReportingOptions) =>
|
|
||||||
error(warning, { ...options, level: 'warning' });
|
|
|
@ -1,4 +1,4 @@
|
||||||
export { ApplicationError } from './application.error';
|
export { ApplicationError, ReportingOptions } from './application.error';
|
||||||
export { ExpressionError } from './expression.error';
|
export { ExpressionError } from './expression.error';
|
||||||
export { CredentialAccessError } from './credential-access-error';
|
export { CredentialAccessError } from './credential-access-error';
|
||||||
export { ExecutionCancelledError } from './execution-cancelled.error';
|
export { ExecutionCancelledError } from './execution-cancelled.error';
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as LoggerProxy from './LoggerProxy';
|
import * as LoggerProxy from './LoggerProxy';
|
||||||
export * as ErrorReporterProxy from './ErrorReporterProxy';
|
|
||||||
export * as ExpressionEvaluatorProxy from './ExpressionEvaluatorProxy';
|
export * as ExpressionEvaluatorProxy from './ExpressionEvaluatorProxy';
|
||||||
import * as NodeHelpers from './NodeHelpers';
|
import * as NodeHelpers from './NodeHelpers';
|
||||||
import * as ObservableObject from './ObservableObject';
|
import * as ObservableObject from './ObservableObject';
|
||||||
|
|
|
@ -1121,6 +1121,9 @@ importers:
|
||||||
'@n8n/config':
|
'@n8n/config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../@n8n/config
|
version: link:../@n8n/config
|
||||||
|
'@sentry/node':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 8.42.0
|
||||||
aws4:
|
aws4:
|
||||||
specifier: 1.11.0
|
specifier: 1.11.0
|
||||||
version: 1.11.0
|
version: 1.11.0
|
||||||
|
|
Loading…
Reference in a new issue