mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
feat(core): Extend error hierarchy
Introduce new error classes: - BaseError : Base class for all other errors - UnexpectedError : Error that was caused a programmer error - OperationalError : A transient error like timeout - UserError : Error that was caused by user's action or input Replace all usages of `ApplicationError` in `cli/src/commands` to the new classes. Up next: - Convert the `ExecutionBaseError` hierarchy to use the new base class - Replace all usages of `ApplicationError` with the new error classes
This commit is contained in:
parent
b1940268e6
commit
ca002b6830
|
@ -22,12 +22,7 @@ import type {
|
|||
WorkflowExecuteMode,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
Workflow,
|
||||
WorkflowActivationError,
|
||||
WebhookPathTakenError,
|
||||
ApplicationError,
|
||||
} from 'n8n-workflow';
|
||||
import { Workflow, WorkflowActivationError, WebhookPathTakenError } from 'n8n-workflow';
|
||||
import { Service } from 'typedi';
|
||||
|
||||
import { ActivationErrorsService } from '@/activation-errors.service';
|
||||
|
@ -228,14 +223,10 @@ export class ActiveWorkflowManager {
|
|||
* deregister those webhooks from external services.
|
||||
*/
|
||||
async clearWebhooks(workflowId: string) {
|
||||
const workflowData = await this.workflowRepository.findOne({
|
||||
const workflowData = await this.workflowRepository.findOneOrFail({
|
||||
where: { id: workflowId },
|
||||
});
|
||||
|
||||
if (workflowData === null) {
|
||||
throw new ApplicationError('Could not find workflow', { extra: { workflowId } });
|
||||
}
|
||||
|
||||
const workflow = new Workflow({
|
||||
id: workflowId,
|
||||
name: workflowData.name,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { PushPayload } from '@n8n/api-types';
|
||||
import { ErrorReporter } from 'n8n-core';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
import { Service } from 'typedi';
|
||||
|
||||
import { CollaborationState } from '@/collaboration/collaboration.state';
|
||||
|
@ -34,7 +34,7 @@ export class CollaborationService {
|
|||
await this.handleUserMessage(event.userId, event.msg);
|
||||
} catch (error) {
|
||||
this.errorReporter.error(
|
||||
new ApplicationError('Error handling CollaborationService push message', {
|
||||
new UnexpectedError('Error handling CollaborationService push message', {
|
||||
extra: {
|
||||
msg: event.msg,
|
||||
userId: event.userId,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SecurityConfig } from '@n8n/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
import { RISK_CATEGORIES } from '@/security-audit/constants';
|
||||
|
@ -48,7 +48,7 @@ export class SecurityAudit extends BaseCommand {
|
|||
|
||||
const hint = `Valid categories are: ${RISK_CATEGORIES.join(', ')}`;
|
||||
|
||||
throw new ApplicationError([message, hint].join('. '));
|
||||
throw new UserError(`${message}. ${hint}`);
|
||||
}
|
||||
|
||||
const result = await Container.get(SecurityAuditService).run(
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
DataDeduplicationService,
|
||||
ErrorReporter,
|
||||
} from 'n8n-core';
|
||||
import { ApplicationError, ensureError, sleep } from 'n8n-workflow';
|
||||
import { ensureError, sleep, ConfigurationError } from 'n8n-workflow';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
import type { AbstractServer } from '@/abstract-server';
|
||||
|
@ -143,7 +143,7 @@ export abstract class BaseCommand extends Command {
|
|||
if (!isSelected && !isAvailable) return;
|
||||
|
||||
if (isSelected && !isAvailable) {
|
||||
throw new ApplicationError(
|
||||
throw new ConfigurationError(
|
||||
'External storage selected but unavailable. Please make external storage available by adding "s3" to `N8N_AVAILABLE_BINARY_DATA_MODES`.',
|
||||
);
|
||||
}
|
||||
|
@ -187,31 +187,31 @@ export abstract class BaseCommand extends Command {
|
|||
const { host, bucket, credentials } = this.globalConfig.externalStorage.s3;
|
||||
|
||||
if (host === '') {
|
||||
throw new ApplicationError(
|
||||
throw new ConfigurationError(
|
||||
'External storage host not configured. Please set `N8N_EXTERNAL_STORAGE_S3_HOST`.',
|
||||
);
|
||||
}
|
||||
|
||||
if (bucket.name === '') {
|
||||
throw new ApplicationError(
|
||||
throw new ConfigurationError(
|
||||
'External storage bucket name not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_NAME`.',
|
||||
);
|
||||
}
|
||||
|
||||
if (bucket.region === '') {
|
||||
throw new ApplicationError(
|
||||
throw new ConfigurationError(
|
||||
'External storage bucket region not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_REGION`.',
|
||||
);
|
||||
}
|
||||
|
||||
if (credentials.accessKey === '') {
|
||||
throw new ApplicationError(
|
||||
throw new ConfigurationError(
|
||||
'External storage access key not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCESS_KEY`.',
|
||||
);
|
||||
}
|
||||
|
||||
if (credentials.accessSecret === '') {
|
||||
throw new ApplicationError(
|
||||
throw new ConfigurationError(
|
||||
'External storage access secret not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCESS_SECRET`.',
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import fs from 'fs';
|
|||
import { diff } from 'json-diff';
|
||||
import pick from 'lodash/pick';
|
||||
import type { IRun, ITaskData, IWorkflowExecutionDataProcess } from 'n8n-workflow';
|
||||
import { ApplicationError, jsonParse } from 'n8n-workflow';
|
||||
import { jsonParse, UnexpectedError } from 'n8n-workflow';
|
||||
import os from 'os';
|
||||
import { sep } from 'path';
|
||||
import { Container } from 'typedi';
|
||||
|
@ -473,7 +473,7 @@ export class ExecuteBatch extends BaseCommand {
|
|||
this.updateStatus();
|
||||
}
|
||||
} else {
|
||||
throw new ApplicationError('Wrong execution status - cannot proceed');
|
||||
throw new UnexpectedError('Wrong execution status - cannot proceed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Flags } from '@oclif/core';
|
||||
import type { IWorkflowBase, IWorkflowExecutionDataProcess } from 'n8n-workflow';
|
||||
import { ApplicationError, ExecutionBaseError } from 'n8n-workflow';
|
||||
import { ExecutionBaseError, UnexpectedError, UserError } from 'n8n-workflow';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
import { ActiveExecutions } from '@/active-executions';
|
||||
|
@ -44,9 +44,8 @@ export class Execute extends BaseCommand {
|
|||
}
|
||||
|
||||
if (flags.file) {
|
||||
throw new ApplicationError(
|
||||
throw new UserError(
|
||||
'The --file flag is no longer supported. Please first import the workflow and then execute it using the --id flag.',
|
||||
{ level: 'warning' },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -64,7 +63,7 @@ export class Execute extends BaseCommand {
|
|||
}
|
||||
|
||||
if (!workflowData) {
|
||||
throw new ApplicationError('Failed to retrieve workflow data for requested workflow');
|
||||
throw new UserError('Failed to retrieve workflow data for requested workflow');
|
||||
}
|
||||
|
||||
if (!isWorkflowIdValid(workflowId)) {
|
||||
|
@ -87,7 +86,7 @@ export class Execute extends BaseCommand {
|
|||
const data = await activeExecutions.getPostExecutePromise(executionId);
|
||||
|
||||
if (data === undefined) {
|
||||
throw new ApplicationError('Workflow did not return any data');
|
||||
throw new UnexpectedError('Workflow did not return any data');
|
||||
}
|
||||
|
||||
if (data.data.resultData.error) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Flags } from '@oclif/core';
|
||||
import fs from 'fs';
|
||||
import { Credentials } from 'n8n-core';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import path from 'path';
|
||||
import Container from 'typedi';
|
||||
|
||||
|
@ -123,7 +123,7 @@ export class ExportCredentialsCommand extends BaseCommand {
|
|||
}
|
||||
|
||||
if (credentials.length === 0) {
|
||||
throw new ApplicationError('No credentials found with specified filters');
|
||||
throw new UserError('No credentials found with specified filters');
|
||||
}
|
||||
|
||||
if (flags.separate) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Flags } from '@oclif/core';
|
||||
import fs from 'fs';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import path from 'path';
|
||||
import Container from 'typedi';
|
||||
|
||||
|
@ -107,7 +107,7 @@ export class ExportWorkflowsCommand extends BaseCommand {
|
|||
});
|
||||
|
||||
if (workflows.length === 0) {
|
||||
throw new ApplicationError('No workflows found with specified filters');
|
||||
throw new UserError('No workflows found with specified filters');
|
||||
}
|
||||
|
||||
if (flags.separate) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import glob from 'fast-glob';
|
|||
import fs from 'fs';
|
||||
import { Cipher } from 'n8n-core';
|
||||
import type { ICredentialsEncrypted } from 'n8n-workflow';
|
||||
import { ApplicationError, jsonParse } from 'n8n-workflow';
|
||||
import { jsonParse, UserError } from 'n8n-workflow';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
import { UM_FIX_INSTRUCTION } from '@/constants';
|
||||
|
@ -66,7 +66,7 @@ export class ImportCredentialsCommand extends BaseCommand {
|
|||
}
|
||||
|
||||
if (flags.projectId && flags.userId) {
|
||||
throw new ApplicationError(
|
||||
throw new UserError(
|
||||
'You cannot use `--userId` and `--projectId` together. Use one or the other.',
|
||||
);
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ export class ImportCredentialsCommand extends BaseCommand {
|
|||
const result = await this.checkRelations(credentials, flags.projectId, flags.userId);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ApplicationError(result.message);
|
||||
throw new UserError(result.message);
|
||||
}
|
||||
|
||||
for (const credential of credentials) {
|
||||
|
@ -202,7 +202,7 @@ export class ImportCredentialsCommand extends BaseCommand {
|
|||
);
|
||||
|
||||
if (!Array.isArray(credentialsUnchecked)) {
|
||||
throw new ApplicationError(
|
||||
throw new UserError(
|
||||
'File does not seem to contain credentials. Make sure the credentials are contained in an array.',
|
||||
);
|
||||
}
|
||||
|
@ -252,7 +252,7 @@ export class ImportCredentialsCommand extends BaseCommand {
|
|||
if (!userId) {
|
||||
const owner = await this.transactionManager.findOneBy(User, { role: 'global:owner' });
|
||||
if (!owner) {
|
||||
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
|
||||
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
|
||||
}
|
||||
userId = owner.id;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Flags } from '@oclif/core';
|
||||
import glob from 'fast-glob';
|
||||
import fs from 'fs';
|
||||
import { ApplicationError, jsonParse } from 'n8n-workflow';
|
||||
import { jsonParse, UserError } from 'n8n-workflow';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
import { UM_FIX_INSTRUCTION } from '@/constants';
|
||||
|
@ -18,7 +18,7 @@ import { BaseCommand } from '../base-command';
|
|||
|
||||
function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] {
|
||||
if (!Array.isArray(workflows)) {
|
||||
throw new ApplicationError(
|
||||
throw new UserError(
|
||||
'File does not seem to contain workflows. Make sure the workflows are contained in an array.',
|
||||
);
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IW
|
|||
!Object.prototype.hasOwnProperty.call(workflow, 'nodes') ||
|
||||
!Object.prototype.hasOwnProperty.call(workflow, 'connections')
|
||||
) {
|
||||
throw new ApplicationError('File does not seem to contain valid workflows.');
|
||||
throw new UserError('File does not seem to contain valid workflows.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
|
|||
}
|
||||
|
||||
if (flags.projectId && flags.userId) {
|
||||
throw new ApplicationError(
|
||||
throw new UserError(
|
||||
'You cannot use `--userId` and `--projectId` together. Use one or the other.',
|
||||
);
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
|
|||
const result = await this.checkRelations(workflows, flags.projectId, flags.userId);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ApplicationError(result.message);
|
||||
throw new UserError(result.message);
|
||||
}
|
||||
|
||||
this.logger.info(`Importing ${workflows.length} workflows...`);
|
||||
|
@ -219,7 +219,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
|
|||
if (!userId) {
|
||||
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
|
||||
if (!owner) {
|
||||
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
|
||||
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
|
||||
}
|
||||
userId = owner.id;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In } from '@n8n/typeorm';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import Container from 'typedi';
|
||||
|
||||
import { UM_FIX_INSTRUCTION } from '@/constants';
|
||||
|
@ -56,7 +56,7 @@ export class Reset extends BaseCommand {
|
|||
Number(!!flags.deleteWorkflowsAndCredentials);
|
||||
|
||||
if (numberOfOptions !== 1) {
|
||||
throw new ApplicationError(wrongFlagsError);
|
||||
throw new UserError(wrongFlagsError);
|
||||
}
|
||||
|
||||
const owner = await this.getOwner();
|
||||
|
@ -71,13 +71,13 @@ export class Reset extends BaseCommand {
|
|||
// Migrate all workflows and credentials to another project.
|
||||
if (flags.projectId ?? flags.userId) {
|
||||
if (flags.userId && ldapIdentities.some((i) => i.userId === flags.userId)) {
|
||||
throw new ApplicationError(
|
||||
throw new UserError(
|
||||
`Can't migrate workflows and credentials to the user with the ID ${flags.userId}. That user was created via LDAP and will be deleted as well.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (flags.projectId && personalProjectIds.includes(flags.projectId)) {
|
||||
throw new ApplicationError(
|
||||
throw new UserError(
|
||||
`Can't migrate workflows and credentials to the project with the ID ${flags.projectId}. That project is a personal project belonging to a user that was created via LDAP and will be deleted as well.`,
|
||||
);
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ export class Reset extends BaseCommand {
|
|||
const project = await Container.get(ProjectRepository).findOneBy({ id: projectId });
|
||||
|
||||
if (project === null) {
|
||||
throw new ApplicationError(`Could not find the project with the ID ${projectId}.`);
|
||||
throw new UserError(`Could not find the project with the ID ${projectId}.`);
|
||||
}
|
||||
|
||||
return project;
|
||||
|
@ -142,7 +142,7 @@ export class Reset extends BaseCommand {
|
|||
const project = await Container.get(ProjectRepository).getPersonalProjectForUser(userId);
|
||||
|
||||
if (project === null) {
|
||||
throw new ApplicationError(
|
||||
throw new UserError(
|
||||
`Could not find the user with the ID ${userId} or their personalProject.`,
|
||||
);
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ export class Reset extends BaseCommand {
|
|||
return project;
|
||||
}
|
||||
|
||||
throw new ApplicationError(wrongFlagsError);
|
||||
throw new UserError(wrongFlagsError);
|
||||
}
|
||||
|
||||
async catch(error: Error): Promise<void> {
|
||||
|
@ -161,7 +161,7 @@ export class Reset extends BaseCommand {
|
|||
private async getOwner() {
|
||||
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
|
||||
if (!owner) {
|
||||
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
|
||||
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
|
||||
}
|
||||
|
||||
return owner;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Flags } from '@oclif/core';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
import { ActiveExecutions } from '@/active-executions';
|
||||
|
@ -84,9 +84,7 @@ export class Webhook extends BaseCommand {
|
|||
|
||||
async run() {
|
||||
if (this.globalConfig.multiMainSetup.enabled) {
|
||||
throw new ApplicationError(
|
||||
'Webhook process cannot be started when multi-main setup is enabled.',
|
||||
);
|
||||
throw new UserError('Webhook process cannot be started when multi-main setup is enabled.');
|
||||
}
|
||||
|
||||
const { ScalingService } = await import('@/scaling/scaling.service');
|
||||
|
|
|
@ -2,7 +2,12 @@ import type { NodeOptions } from '@sentry/node';
|
|||
import { close } from '@sentry/node';
|
||||
import type { ErrorEvent, EventHint } from '@sentry/types';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ApplicationError, ExecutionCancelledError, type ReportingOptions } from 'n8n-workflow';
|
||||
import {
|
||||
ApplicationError,
|
||||
isBaseError,
|
||||
ExecutionCancelledError,
|
||||
type ReportingOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { Service } from 'typedi';
|
||||
|
||||
|
@ -29,11 +34,15 @@ export class ErrorReporter {
|
|||
const context = executionId ? ` (execution ${executionId})` : '';
|
||||
|
||||
do {
|
||||
const msg = [
|
||||
e.message + context,
|
||||
e instanceof ApplicationError && e.level === 'error' && e.stack ? `\n${e.stack}\n` : '',
|
||||
].join('');
|
||||
const meta = e instanceof ApplicationError ? e.extra : undefined;
|
||||
let stack = '';
|
||||
let meta = undefined;
|
||||
if (e instanceof ApplicationError || isBaseError(e)) {
|
||||
if (e.level === 'error' && e.stack) {
|
||||
stack = `\n${e.stack}\n`;
|
||||
}
|
||||
meta = e.extra;
|
||||
}
|
||||
const msg = [e.message + context, stack].join('');
|
||||
this.logger.error(msg, meta);
|
||||
e = e.cause as Error;
|
||||
} while (e);
|
||||
|
@ -117,13 +126,8 @@ export class ErrorReporter {
|
|||
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 (this.handleBaseError(event, originalException)) return null;
|
||||
if (this.handleApplicationError(event, originalException)) return null;
|
||||
|
||||
if (
|
||||
originalException instanceof Error &&
|
||||
|
@ -164,4 +168,31 @@ export class ErrorReporter {
|
|||
if (typeof e === 'string') return new ApplicationError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
/** @returns Whether the error should be dropped */
|
||||
private handleBaseError(event: ErrorEvent, error: unknown): boolean {
|
||||
if (isBaseError(error)) {
|
||||
if (!error.shouldReport) return true;
|
||||
|
||||
event.level = error.level;
|
||||
if (error.extra) event.extra = { ...event.extra, ...error.extra };
|
||||
if (error.tags) event.tags = { ...event.tags, ...error.tags };
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @returns Whether the error should be dropped */
|
||||
private handleApplicationError(event: ErrorEvent, error: unknown): boolean {
|
||||
if (error instanceof ApplicationError) {
|
||||
const { level, extra, tags } = error;
|
||||
if (level === 'warning') return true;
|
||||
|
||||
event.level = level;
|
||||
if (extra) event.extra = { ...event.extra, ...extra };
|
||||
if (tags) event.tags = { ...event.tags, ...tags };
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ 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 { ApplicationError, BaseError } from 'n8n-workflow';
|
||||
|
||||
import { ErrorReporter } from '@/error-reporter';
|
||||
import type { Logger } from '@/logging/logger';
|
||||
|
@ -101,6 +101,43 @@ describe('ErrorReporter', () => {
|
|||
const result = await errorReporter.beforeSend(event, { originalException });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
describe('BaseError', () => {
|
||||
class TestError extends BaseError {}
|
||||
|
||||
it('should drop errors with shouldReport false', async () => {
|
||||
const originalException = new TestError('test', { shouldReport: false });
|
||||
|
||||
expect(await errorReporter.beforeSend(event, { originalException })).toEqual(null);
|
||||
});
|
||||
|
||||
it('should keep events with shouldReport true', async () => {
|
||||
const originalException = new TestError('test', { shouldReport: true });
|
||||
|
||||
expect(await errorReporter.beforeSend(event, { originalException })).toEqual(event);
|
||||
});
|
||||
|
||||
it('should set level, extra, and tags from BaseError', async () => {
|
||||
const originalException = new TestError('Test error', {
|
||||
level: 'error',
|
||||
extra: { foo: 'bar' },
|
||||
tags: { tag1: 'value1' },
|
||||
});
|
||||
|
||||
const testEvent = {} as ErrorEvent;
|
||||
|
||||
const result = await errorReporter.beforeSend(testEvent, { originalException });
|
||||
|
||||
expect(result).toEqual({
|
||||
level: 'error',
|
||||
extra: { foo: 'bar' },
|
||||
tags: {
|
||||
packageName: 'core',
|
||||
tag1: 'value1',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { Functionality, IDataObject, JsonObject } from '../../Interfaces';
|
||||
import { ApplicationError, type ReportingOptions } from '../application.error';
|
||||
import { ApplicationError } from '../application.error';
|
||||
import type { ReportingOptions } from '../error.types';
|
||||
|
||||
interface ExecutionBaseErrorOptions extends ReportingOptions {
|
||||
cause?: Error;
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import type { Event } from '@sentry/node';
|
||||
import callsites from 'callsites';
|
||||
|
||||
export type Level = 'warning' | 'error' | 'fatal' | 'info';
|
||||
|
||||
export type ReportingOptions = {
|
||||
level?: Level;
|
||||
executionId?: string;
|
||||
} & Pick<Event, 'tags' | 'extra'>;
|
||||
import type { ErrorLevel, ReportingOptions } from '@/errors/error.types';
|
||||
|
||||
/**
|
||||
* @deprecated Use `UserError`, `OperationalError` or `UnexpectedError` instead.
|
||||
*/
|
||||
export class ApplicationError extends Error {
|
||||
level: Level;
|
||||
level: ErrorLevel;
|
||||
|
||||
readonly tags: NonNullable<Event['tags']>;
|
||||
|
||||
|
|
62
packages/workflow/src/errors/base/base.error.ts
Normal file
62
packages/workflow/src/errors/base/base.error.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import type { Event } from '@sentry/node';
|
||||
import callsites from 'callsites';
|
||||
|
||||
import type { ErrorTags, ErrorLevel, ReportingOptions } from '../error.types';
|
||||
|
||||
export type BaseErrorOptions = { description?: undefined | null } & ErrorOptions & ReportingOptions;
|
||||
|
||||
/**
|
||||
* Base class for all errors
|
||||
*/
|
||||
export abstract class BaseError extends Error {
|
||||
/**
|
||||
* Error level. Defines which level the error should be logged/reported
|
||||
* @default 'error'
|
||||
*/
|
||||
readonly level: ErrorLevel;
|
||||
|
||||
/**
|
||||
* Whether the error should be reported to Sentry.
|
||||
* @default true
|
||||
*/
|
||||
readonly shouldReport: boolean;
|
||||
|
||||
readonly description: string | null | undefined;
|
||||
|
||||
readonly tags: ErrorTags;
|
||||
|
||||
readonly extra?: Event['extra'];
|
||||
|
||||
readonly packageName?: string;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
{
|
||||
level = 'error',
|
||||
description,
|
||||
shouldReport = true,
|
||||
tags = {},
|
||||
extra,
|
||||
...rest
|
||||
}: BaseErrorOptions = {},
|
||||
) {
|
||||
super(message, rest);
|
||||
|
||||
this.level = level;
|
||||
this.shouldReport = shouldReport;
|
||||
this.description = description;
|
||||
this.tags = tags;
|
||||
this.extra = extra;
|
||||
|
||||
try {
|
||||
const filePath = callsites()[2].getFileName() ?? '';
|
||||
const match = /packages\/([^\/]+)\//.exec(filePath)?.[1];
|
||||
|
||||
if (match) this.tags.packageName = match;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
export function isBaseError(error: unknown): error is BaseError {
|
||||
return error instanceof BaseError;
|
||||
}
|
28
packages/workflow/src/errors/base/operational.error.ts
Normal file
28
packages/workflow/src/errors/base/operational.error.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import type { BaseErrorOptions } from './base.error';
|
||||
import { BaseError } from './base.error';
|
||||
|
||||
export type OperationalErrorOptions = Omit<BaseErrorOptions, 'level'> & {
|
||||
level?: 'info' | 'warning' | 'error';
|
||||
};
|
||||
|
||||
/**
|
||||
* Error that indicates a transient issue, like a network request failing,
|
||||
* a database query timing out, etc. These are expected to happen, are
|
||||
* transient by nature and should be handled gracefully.
|
||||
*
|
||||
* Default level: warning
|
||||
* Default shouldReport: false
|
||||
*/
|
||||
export class OperationalError extends BaseError {
|
||||
constructor(message: string, opts: OperationalErrorOptions = {}) {
|
||||
opts.level = opts.level ?? 'warning';
|
||||
opts.shouldReport = opts.shouldReport ?? false;
|
||||
|
||||
super(message, opts);
|
||||
}
|
||||
}
|
||||
|
||||
/** Convenience function to check if an error is an instance of OperationalError */
|
||||
export function isOperationalError(error: Error): error is OperationalError {
|
||||
return error instanceof OperationalError;
|
||||
}
|
28
packages/workflow/src/errors/base/unexpected.error.ts
Normal file
28
packages/workflow/src/errors/base/unexpected.error.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import type { BaseErrorOptions } from './base.error';
|
||||
import { BaseError } from './base.error';
|
||||
|
||||
export type UnexpectedErrorOptions = Omit<BaseErrorOptions, 'level'> & {
|
||||
level?: 'error' | 'fatal';
|
||||
};
|
||||
|
||||
/**
|
||||
* Error that indicates something is wrong in the code: logic mistakes,
|
||||
* unhandled cases, assertions that fail. These are not recoverable and
|
||||
* should be brought to developers' attention.
|
||||
*
|
||||
* Default level: error
|
||||
* Default shouldReport: true
|
||||
*/
|
||||
export class UnexpectedError extends BaseError {
|
||||
constructor(message: string, opts: UnexpectedErrorOptions = {}) {
|
||||
opts.level = opts.level ?? 'error';
|
||||
opts.shouldReport = opts.shouldReport ?? true;
|
||||
|
||||
super(message, opts);
|
||||
}
|
||||
}
|
||||
|
||||
/** Convenience function to check if an error is an instance of UnexpectedError */
|
||||
export function isUnexpectedError(error: Error): error is UnexpectedError {
|
||||
return error instanceof UnexpectedError;
|
||||
}
|
31
packages/workflow/src/errors/base/user.error.ts
Normal file
31
packages/workflow/src/errors/base/user.error.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import type { BaseErrorOptions } from './base.error';
|
||||
import { BaseError } from './base.error';
|
||||
|
||||
export type UserErrorOptions = Omit<BaseErrorOptions, 'level'> & {
|
||||
level?: 'info' | 'warning';
|
||||
description?: string | null | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Error that indicates the user performed an action that caused an error.
|
||||
* E.g. provided invalid input, tried to access a resource they’re not
|
||||
* authorized to, or violates a business rule.
|
||||
*
|
||||
* Default level: info
|
||||
* Default shouldReport: false
|
||||
*/
|
||||
export class UserError extends BaseError {
|
||||
readonly description: string | null | undefined;
|
||||
|
||||
constructor(message: string, opts: UserErrorOptions = {}) {
|
||||
opts.level = opts.level ?? 'info';
|
||||
opts.shouldReport = opts.shouldReport ?? false;
|
||||
|
||||
super(message, opts);
|
||||
}
|
||||
}
|
||||
|
||||
/** Convenience function to check if an error is an instance of UserError */
|
||||
export function isUserError(error: Error): error is UserError {
|
||||
return error instanceof UserError;
|
||||
}
|
9
packages/workflow/src/errors/configuration.error.ts
Normal file
9
packages/workflow/src/errors/configuration.error.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { UserError } from './base/user.error';
|
||||
|
||||
/**
|
||||
* Error that indicates the user provided invalid configuration.
|
||||
*
|
||||
* Default level: info
|
||||
* Default shouldReport: false
|
||||
*/
|
||||
export class ConfigurationError extends UserError {}
|
14
packages/workflow/src/errors/error.types.ts
Normal file
14
packages/workflow/src/errors/error.types.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import type { Event } from '@sentry/node';
|
||||
|
||||
export type ErrorLevel = 'fatal' | 'error' | 'warning' | 'info';
|
||||
|
||||
export type ErrorTags = NonNullable<Event['tags']>;
|
||||
|
||||
export type ReportingOptions = {
|
||||
/** Whether the error should be reported to Sentry */
|
||||
shouldReport?: boolean;
|
||||
level?: ErrorLevel;
|
||||
tags?: ErrorTags;
|
||||
extra?: Event['extra'];
|
||||
executionId?: string;
|
||||
};
|
|
@ -1,4 +1,10 @@
|
|||
export { ApplicationError, type ReportingOptions } from './application.error';
|
||||
export type * from './error.types';
|
||||
export { BaseError, isBaseError, type BaseErrorOptions } from './base/base.error';
|
||||
export { OperationalError, type OperationalErrorOptions } from './base/operational.error';
|
||||
export { UnexpectedError, type UnexpectedErrorOptions } from './base/unexpected.error';
|
||||
export { UserError, type UserErrorOptions } from './base/user.error';
|
||||
export { ConfigurationError } from './configuration.error';
|
||||
export { ApplicationError } from './application.error';
|
||||
export { ExpressionError } from './expression.error';
|
||||
export { CredentialAccessError } from './credential-access-error';
|
||||
export { ExecutionCancelledError } from './execution-cancelled.error';
|
||||
|
|
|
@ -5,7 +5,7 @@ import { AxiosError } from 'axios';
|
|||
import { parseString } from 'xml2js';
|
||||
|
||||
import { NodeError } from './abstract/node.error';
|
||||
import type { ReportingOptions } from './application.error';
|
||||
import type { ErrorLevel } from './error.types';
|
||||
import {
|
||||
NO_OP_NODE_TYPE,
|
||||
UNKNOWN_ERROR_DESCRIPTION,
|
||||
|
@ -26,7 +26,7 @@ export interface NodeOperationErrorOptions {
|
|||
description?: string;
|
||||
runIndex?: number;
|
||||
itemIndex?: number;
|
||||
level?: ReportingOptions['level'];
|
||||
level?: ErrorLevel;
|
||||
messageMapping?: { [key: string]: string }; // allows to pass custom mapping for error messages scoped to a node
|
||||
functionality?: Functionality;
|
||||
type?: string;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { ApplicationError, type Level } from './application.error';
|
||||
import { ApplicationError } from './application.error';
|
||||
import type { ErrorLevel } from './error.types';
|
||||
import type { INode } from '../Interfaces';
|
||||
|
||||
interface TriggerCloseErrorOptions extends ErrorOptions {
|
||||
level: Level;
|
||||
level: ErrorLevel;
|
||||
}
|
||||
|
||||
export class TriggerCloseError extends ApplicationError {
|
||||
|
|
36
packages/workflow/test/errors/base/operational.error.test.ts
Normal file
36
packages/workflow/test/errors/base/operational.error.test.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { BaseError } from '@/errors/base/base.error';
|
||||
import { isOperationalError, OperationalError } from '@/errors/base/operational.error';
|
||||
|
||||
describe('OperationalError', () => {
|
||||
it('should be an instance of OperationalError', () => {
|
||||
const error = new OperationalError('test');
|
||||
expect(error).toBeInstanceOf(OperationalError);
|
||||
});
|
||||
|
||||
it('should be an instance of BaseError', () => {
|
||||
const error = new OperationalError('test');
|
||||
expect(error).toBeInstanceOf(BaseError);
|
||||
});
|
||||
|
||||
it('should have correct defaults', () => {
|
||||
const error = new OperationalError('test');
|
||||
expect(error.level).toBe('warning');
|
||||
expect(error.shouldReport).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow overriding the default level and shouldReport', () => {
|
||||
const error = new OperationalError('test', { level: 'error', shouldReport: true });
|
||||
expect(error.level).toBe('error');
|
||||
expect(error.shouldReport).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOperationalError', () => {
|
||||
it('should return true if the error is an instance of OperationalError', () => {
|
||||
expect(isOperationalError(new OperationalError('test'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the error is not an instance of OperationalError', () => {
|
||||
expect(isOperationalError(new Error('test'))).toBe(false);
|
||||
});
|
||||
});
|
36
packages/workflow/test/errors/base/unexpected.error.test.ts
Normal file
36
packages/workflow/test/errors/base/unexpected.error.test.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { BaseError } from '@/errors/base/base.error';
|
||||
import { isUnexpectedError, UnexpectedError } from '@/errors/base/unexpected.error';
|
||||
|
||||
describe('UnexpectedError', () => {
|
||||
it('should be an instance of UnexpectedError', () => {
|
||||
const error = new UnexpectedError('test');
|
||||
expect(error).toBeInstanceOf(UnexpectedError);
|
||||
});
|
||||
|
||||
it('should be an instance of BaseError', () => {
|
||||
const error = new UnexpectedError('test');
|
||||
expect(error).toBeInstanceOf(BaseError);
|
||||
});
|
||||
|
||||
it('should have correct defaults', () => {
|
||||
const error = new UnexpectedError('test');
|
||||
expect(error.level).toBe('error');
|
||||
expect(error.shouldReport).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow overriding the default level and shouldReport', () => {
|
||||
const error = new UnexpectedError('test', { level: 'fatal', shouldReport: false });
|
||||
expect(error.level).toBe('fatal');
|
||||
expect(error.shouldReport).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUnexpectedError', () => {
|
||||
it('should return true if the error is an instance of UnexpectedError', () => {
|
||||
expect(isUnexpectedError(new UnexpectedError('test'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the error is not an instance of UnexpectedError', () => {
|
||||
expect(isUnexpectedError(new Error('test'))).toBe(false);
|
||||
});
|
||||
});
|
37
packages/workflow/test/errors/base/user.error.test.ts
Normal file
37
packages/workflow/test/errors/base/user.error.test.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { BaseError } from '@/errors/base/base.error';
|
||||
import { isUserError } from '@/errors/base/user.error';
|
||||
import { UserError } from '@/errors/base/user.error';
|
||||
|
||||
describe('UserError', () => {
|
||||
it('should be an instance of UserError', () => {
|
||||
const error = new UserError('test');
|
||||
expect(error).toBeInstanceOf(UserError);
|
||||
});
|
||||
|
||||
it('should be an instance of BaseError', () => {
|
||||
const error = new UserError('test');
|
||||
expect(error).toBeInstanceOf(BaseError);
|
||||
});
|
||||
|
||||
it('should have correct defaults', () => {
|
||||
const error = new UserError('test');
|
||||
expect(error.level).toBe('info');
|
||||
expect(error.shouldReport).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow overriding the default level and shouldReport', () => {
|
||||
const error = new UserError('test', { level: 'warning', shouldReport: true });
|
||||
expect(error.level).toBe('warning');
|
||||
expect(error.shouldReport).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUserError', () => {
|
||||
it('should return true if the error is an instance of UserError', () => {
|
||||
expect(isUserError(new UserError('test'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the error is not an instance of UserError', () => {
|
||||
expect(isUserError(new Error('test'))).toBe(false);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue