mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(core): Extend error hierarchy (#12267)
This commit is contained in:
parent
1e1f528466
commit
2ab59d775b
|
@ -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 type { Logger } from '@/logging/logger';
|
||||
|
||||
|
@ -133,6 +133,43 @@ describe('ErrorReporter', () => {
|
|||
expect(beforeSendFilter).toHaveBeenCalledWith(event, hint);
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
|
|
@ -3,7 +3,8 @@ 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 type { ReportingOptions } from 'n8n-workflow';
|
||||
import { ApplicationError, ExecutionCancelledError, BaseError } from 'n8n-workflow';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import type { InstanceType } from '@/instance-settings';
|
||||
|
@ -44,11 +45,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 || e instanceof BaseError) {
|
||||
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);
|
||||
|
@ -137,11 +142,17 @@ export class ErrorReporter {
|
|||
|
||||
if (originalException instanceof AxiosError) return null;
|
||||
|
||||
if (originalException instanceof BaseError) {
|
||||
if (!originalException.shouldReport) return null;
|
||||
|
||||
this.extractEventDetailsFromN8nError(event, originalException);
|
||||
}
|
||||
|
||||
if (this.isIgnoredSqliteError(originalException)) return null;
|
||||
if (this.isApplicationError(originalException)) {
|
||||
if (originalException instanceof ApplicationError) {
|
||||
if (this.isIgnoredApplicationError(originalException)) return null;
|
||||
|
||||
this.extractEventDetailsFromApplicationError(event, originalException);
|
||||
this.extractEventDetailsFromN8nError(event, originalException);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -193,17 +204,13 @@ export class ErrorReporter {
|
|||
);
|
||||
}
|
||||
|
||||
private isApplicationError(error: unknown): error is ApplicationError {
|
||||
return error instanceof ApplicationError;
|
||||
}
|
||||
|
||||
private isIgnoredApplicationError(error: ApplicationError) {
|
||||
return error.level === 'warning';
|
||||
}
|
||||
|
||||
private extractEventDetailsFromApplicationError(
|
||||
private extractEventDetailsFromN8nError(
|
||||
event: ErrorEvent,
|
||||
originalException: ApplicationError,
|
||||
originalException: ApplicationError | BaseError,
|
||||
) {
|
||||
const { level, extra, tags } = originalException;
|
||||
event.level = level;
|
||||
|
|
|
@ -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']>;
|
||||
|
||||
|
|
58
packages/workflow/src/errors/base/base.error.ts
Normal file
58
packages/workflow/src/errors/base/base.error.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
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,
|
||||
tags = {},
|
||||
extra,
|
||||
...rest
|
||||
}: BaseErrorOptions = {},
|
||||
) {
|
||||
super(message, rest);
|
||||
|
||||
this.level = level;
|
||||
this.shouldReport = shouldReport ?? (level === 'error' || level === 'fatal');
|
||||
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 {}
|
||||
}
|
||||
}
|
21
packages/workflow/src/errors/base/operational.error.ts
Normal file
21
packages/workflow/src/errors/base/operational.error.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
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
|
||||
*/
|
||||
export class OperationalError extends BaseError {
|
||||
constructor(message: string, opts: OperationalErrorOptions = {}) {
|
||||
opts.level = opts.level ?? 'warning';
|
||||
|
||||
super(message, opts);
|
||||
}
|
||||
}
|
21
packages/workflow/src/errors/base/unexpected.error.ts
Normal file
21
packages/workflow/src/errors/base/unexpected.error.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
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
|
||||
*/
|
||||
export class UnexpectedError extends BaseError {
|
||||
constructor(message: string, opts: UnexpectedErrorOptions = {}) {
|
||||
opts.level = opts.level ?? 'error';
|
||||
|
||||
super(message, opts);
|
||||
}
|
||||
}
|
24
packages/workflow/src/errors/base/user.error.ts
Normal file
24
packages/workflow/src/errors/base/user.error.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
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
|
||||
*/
|
||||
export class UserError extends BaseError {
|
||||
readonly description: string | null | undefined;
|
||||
|
||||
constructor(message: string, opts: UserErrorOptions = {}) {
|
||||
opts.level = opts.level ?? 'info';
|
||||
|
||||
super(message, opts);
|
||||
}
|
||||
}
|
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,9 @@
|
|||
export { ApplicationError, type ReportingOptions } from './application.error';
|
||||
export type * from './error.types';
|
||||
export { BaseError, 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 { 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,
|
||||
|
@ -27,7 +27,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 {
|
||||
|
|
26
packages/workflow/test/errors/base/operational.error.test.ts
Normal file
26
packages/workflow/test/errors/base/operational.error.test.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { BaseError } from '@/errors/base/base.error';
|
||||
import { 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);
|
||||
});
|
||||
});
|
26
packages/workflow/test/errors/base/unexpected.error.test.ts
Normal file
26
packages/workflow/test/errors/base/unexpected.error.test.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { BaseError } from '@/errors/base/base.error';
|
||||
import { 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);
|
||||
});
|
||||
});
|
26
packages/workflow/test/errors/base/user.error.test.ts
Normal file
26
packages/workflow/test/errors/base/user.error.test.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { BaseError } from '@/errors/base/base.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);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue