first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import type { Logger } from '@n8n/backend-common';
|
||||
import { QueryFailedError } from '@n8n/typeorm';
|
||||
import type { ErrorEvent } from '@sentry/core';
|
||||
import { AxiosError } from 'axios';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { ApplicationError, BaseError } from 'n8n-workflow';
|
||||
|
||||
import { ErrorReporter } from '../error-reporter';
|
||||
|
||||
jest.mock('@sentry/node', () => ({
|
||||
init: jest.fn(),
|
||||
setTag: jest.fn(),
|
||||
captureException: jest.fn(),
|
||||
Integrations: {},
|
||||
}));
|
||||
|
||||
jest.spyOn(process, 'on');
|
||||
|
||||
describe('ErrorReporter', () => {
|
||||
const errorReporter = new ErrorReporter(mock(), mock());
|
||||
const event = {} as ErrorEvent;
|
||||
|
||||
describe('beforeSend', () => {
|
||||
it('should ignore errors with level warning', async () => {
|
||||
const originalException = new ApplicationError('test');
|
||||
originalException.level = 'warning';
|
||||
|
||||
expect(await errorReporter.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 errorReporter.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 errorReporter.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 errorReporter.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 errorReporter.beforeSend(event, { originalException });
|
||||
expect(firstResult).toEqual(event);
|
||||
|
||||
const secondResult = await errorReporter.beforeSend(event, { originalException });
|
||||
expect(secondResult).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle Promise rejections', async () => {
|
||||
const originalException = Promise.reject(new Error());
|
||||
|
||||
const result = await errorReporter.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 errorReporter.beforeSend(event, { originalException });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
describe('beforeSendFilter', () => {
|
||||
const newErrorReportedWithBeforeSendFilter = (beforeSendFilter: jest.Mock) => {
|
||||
const errorReporter = new ErrorReporter(mock(), mock());
|
||||
// @ts-expect-error - beforeSendFilter is private
|
||||
errorReporter.beforeSendFilter = beforeSendFilter;
|
||||
return errorReporter;
|
||||
};
|
||||
|
||||
it('should filter out based on the beforeSendFilter', async () => {
|
||||
const beforeSendFilter = jest.fn().mockReturnValue(true);
|
||||
const errorReporter = newErrorReportedWithBeforeSendFilter(beforeSendFilter);
|
||||
const hint = { originalException: new Error() };
|
||||
|
||||
const result = await errorReporter.beforeSend(event, hint);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(beforeSendFilter).toHaveBeenCalledWith(event, hint);
|
||||
});
|
||||
|
||||
it('should not filter out when beforeSendFilter returns false', async () => {
|
||||
const beforeSendFilter = jest.fn().mockReturnValue(false);
|
||||
const errorReporter = newErrorReportedWithBeforeSendFilter(beforeSendFilter);
|
||||
const hint = { originalException: new Error() };
|
||||
|
||||
const result = await errorReporter.beforeSend(event, hint);
|
||||
|
||||
expect(result).toEqual(event);
|
||||
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', () => {
|
||||
let error: ApplicationError;
|
||||
let logger: Logger;
|
||||
let errorReporter: ErrorReporter;
|
||||
const metadata = undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
error = new ApplicationError('Test error');
|
||||
logger = mock<Logger>();
|
||||
errorReporter = new ErrorReporter(logger, mock());
|
||||
});
|
||||
|
||||
it('should include stack trace for error-level `ApplicationError`', () => {
|
||||
error.level = 'error';
|
||||
errorReporter.error(error);
|
||||
expect(logger.error).toHaveBeenCalledWith(`Test error\n${error.stack}\n`, metadata);
|
||||
});
|
||||
|
||||
it('should exclude stack trace for warning-level `ApplicationError`', () => {
|
||||
error.level = 'warning';
|
||||
errorReporter.error(error);
|
||||
expect(logger.error).toHaveBeenCalledWith('Test error', metadata);
|
||||
});
|
||||
|
||||
it.each([true, undefined])(
|
||||
'should log the error when shouldBeLogged is %s',
|
||||
(shouldBeLogged) => {
|
||||
error.level = 'error';
|
||||
errorReporter.error(error, { shouldBeLogged });
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
);
|
||||
|
||||
it('should not log the error when shouldBeLogged is false', () => {
|
||||
error.level = 'error';
|
||||
errorReporter.error(error, { shouldBeLogged: false });
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ApplicationError } from '@n8n/errors';
|
||||
|
||||
export abstract class BinaryDataError extends ApplicationError {}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ApplicationError } from '@n8n/errors';
|
||||
|
||||
export abstract class FileSystemError extends ApplicationError {
|
||||
constructor(message: string, filePath: string) {
|
||||
super(message, { extra: { filePath } });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
|
||||
export class BinaryDataFileNotFoundError extends UnexpectedError {
|
||||
constructor(fileId: string) {
|
||||
super('Binary data file not found', { extra: { fileId } });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { FileSystemError } from './abstract/filesystem.error';
|
||||
|
||||
export class DisallowedFilepathError extends FileSystemError {
|
||||
constructor(filePath: string) {
|
||||
super('Disallowed path detected', filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
import { inTest, Logger } from '@n8n/backend-common';
|
||||
import { type InstanceType } from '@n8n/constants';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { ReportingOptions } from '@n8n/errors';
|
||||
import type { ErrorEvent, EventHint } from '@sentry/core';
|
||||
import type { NodeOptions } from '@sentry/node';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ApplicationError, ExecutionCancelledError, BaseError } from 'n8n-workflow';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import { Tracing, SentryTracing } from '@/observability';
|
||||
|
||||
type SentryIntegration = 'Redis' | 'Postgres' | 'Http' | 'Express';
|
||||
|
||||
type ErrorReporterInitOptions = {
|
||||
serverType: InstanceType | 'task_runner';
|
||||
dsn: string;
|
||||
release: string;
|
||||
environment: string;
|
||||
serverName: string;
|
||||
releaseDate?: Date;
|
||||
|
||||
/** Whether to enable event loop block detection, if Sentry is enabled. */
|
||||
withEventLoopBlockDetection: boolean;
|
||||
|
||||
/** Threshold in ms for event loop block detection. Only used if `withEventLoopBlockDetection` is true. */
|
||||
eventLoopBlockThreshold?: number;
|
||||
|
||||
/** Sample rate for Sentry traces (0.0 to 1.0). 0 means disabled */
|
||||
tracesSampleRate: number;
|
||||
|
||||
/** Sample rate for Sentry profiling (0.0 to 1.0). 0 means disabled */
|
||||
profilesSampleRate: number;
|
||||
|
||||
/**
|
||||
* Function to allow filtering out errors before they are sent to Sentry.
|
||||
* Return true if the error should be filtered out.
|
||||
*/
|
||||
beforeSendFilter?: (event: ErrorEvent, hint: EventHint) => boolean;
|
||||
|
||||
/**
|
||||
* Integrations eligible for enablement. `tracesSampleRate` still determines
|
||||
* whether they are actually enabled or not.
|
||||
*/
|
||||
eligibleIntegrations?: Partial<Record<SentryIntegration, boolean>>;
|
||||
|
||||
/** Health endpoint path */
|
||||
healthEndpoint?: string;
|
||||
};
|
||||
|
||||
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
const SIX_WEEKS_IN_MS = 6 * 7 * ONE_DAY_IN_MS;
|
||||
const RELEASE_EXPIRATION_WARNING =
|
||||
'Error tracking disabled because this release is older than 6 weeks.';
|
||||
|
||||
@Service()
|
||||
export class ErrorReporter {
|
||||
private expirationTimer?: NodeJS.Timeout;
|
||||
|
||||
/** Hashes of error stack traces, to deduplicate error reports. */
|
||||
private seenErrors = new Set<string>();
|
||||
|
||||
private report: (error: Error | string, options?: ReportingOptions) => void;
|
||||
|
||||
private beforeSendFilter?: (event: ErrorEvent, hint: EventHint) => boolean;
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly tracing: Tracing,
|
||||
) {
|
||||
// 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 {
|
||||
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('');
|
||||
// Default to logging the error if option is not specified
|
||||
if (options?.shouldBeLogged ?? true) {
|
||||
this.logger.error(msg, meta);
|
||||
}
|
||||
e = e.cause as Error;
|
||||
} while (e);
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown(timeoutInMs = 1000) {
|
||||
clearTimeout(this.expirationTimer);
|
||||
const { close } = await import('@sentry/node');
|
||||
await close(timeoutInMs);
|
||||
}
|
||||
|
||||
async init({
|
||||
beforeSendFilter,
|
||||
dsn,
|
||||
serverType,
|
||||
release,
|
||||
environment,
|
||||
serverName,
|
||||
releaseDate,
|
||||
withEventLoopBlockDetection,
|
||||
eventLoopBlockThreshold,
|
||||
profilesSampleRate,
|
||||
tracesSampleRate,
|
||||
eligibleIntegrations = {},
|
||||
healthEndpoint = '/healthz',
|
||||
}: ErrorReporterInitOptions) {
|
||||
if (inTest) return;
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
this.error(error);
|
||||
});
|
||||
|
||||
if (releaseDate) {
|
||||
const releaseExpiresAtMs = releaseDate.getTime() + SIX_WEEKS_IN_MS;
|
||||
const releaseExpiresInMs = () => releaseExpiresAtMs - Date.now();
|
||||
if (releaseExpiresInMs() <= 0) {
|
||||
this.logger.warn(RELEASE_EXPIRATION_WARNING);
|
||||
return;
|
||||
}
|
||||
const checkForExpiration = () => {
|
||||
// Once this release expires, reject all events
|
||||
if (releaseExpiresInMs() <= 0) {
|
||||
this.logger.warn(RELEASE_EXPIRATION_WARNING);
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.report = this.defaultReport;
|
||||
} else {
|
||||
this.expirationTimer = setTimeout(checkForExpiration, ONE_DAY_IN_MS);
|
||||
}
|
||||
};
|
||||
checkForExpiration();
|
||||
}
|
||||
|
||||
if (!dsn) return;
|
||||
|
||||
// Collect longer stacktraces
|
||||
Error.stackTraceLimit = 50;
|
||||
|
||||
const sentry = await import('@sentry/node');
|
||||
const {
|
||||
init,
|
||||
captureException,
|
||||
setTag,
|
||||
setUser,
|
||||
requestDataIntegration,
|
||||
rewriteFramesIntegration,
|
||||
} = sentry;
|
||||
|
||||
// Most of the integrations are listed here:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/
|
||||
const enabledIntegrations = new Set([
|
||||
'InboundFilters',
|
||||
'FunctionToString',
|
||||
'LinkedErrors',
|
||||
'OnUnhandledRejection',
|
||||
'ContextLines',
|
||||
]);
|
||||
|
||||
const isTracingEnabled = tracesSampleRate > 0;
|
||||
if (isTracingEnabled) {
|
||||
const tracingIntegrations: SentryIntegration[] = ['Http', 'Postgres', 'Redis', 'Express'];
|
||||
tracingIntegrations
|
||||
.filter((integrationName) => !!eligibleIntegrations[integrationName])
|
||||
.forEach((integrationName) => enabledIntegrations.add(integrationName));
|
||||
|
||||
this.tracing.setTracingImplementation(new SentryTracing(sentry));
|
||||
}
|
||||
|
||||
const isProfilingEnabled = profilesSampleRate > 0;
|
||||
if (isProfilingEnabled && !isTracingEnabled) {
|
||||
this.logger.warn('Profiling is enabled but tracing is disabled. Profiling will not work.');
|
||||
}
|
||||
|
||||
const eventLoopBlockIntegration = withEventLoopBlockDetection
|
||||
? // The EventLoopBlockIntegration doesn't automatically include the
|
||||
// same tags, so we set them explicitly.
|
||||
await this.getEventLoopBlockIntegration(
|
||||
{
|
||||
server_name: serverName,
|
||||
server_type: serverType,
|
||||
},
|
||||
eventLoopBlockThreshold,
|
||||
)
|
||||
: [];
|
||||
|
||||
const profilingIntegration = isProfilingEnabled ? await this.getProfilingIntegration() : [];
|
||||
|
||||
init({
|
||||
dsn,
|
||||
release,
|
||||
environment,
|
||||
serverName,
|
||||
...(isTracingEnabled ? { tracesSampleRate } : {}),
|
||||
...(isProfilingEnabled ? { profilesSampleRate, profileLifecycle: 'trace' } : {}),
|
||||
beforeSend: this.beforeSend.bind(this) as NodeOptions['beforeSend'],
|
||||
ignoreTransactions: [`GET ${healthEndpoint}`, 'GET /metrics', 'SET search_path TO'],
|
||||
ignoreSpans: [`GET ${healthEndpoint}`, 'GET /metrics', 'SET search_path TO'],
|
||||
integrations: (integrations) => [
|
||||
...integrations.filter(({ name }) => enabledIntegrations.has(name)),
|
||||
rewriteFramesIntegration({ root: '/' }),
|
||||
requestDataIntegration({
|
||||
include: {
|
||||
cookies: false,
|
||||
data: false,
|
||||
headers: false,
|
||||
query_string: false,
|
||||
url: true,
|
||||
},
|
||||
}),
|
||||
...eventLoopBlockIntegration,
|
||||
...profilingIntegration,
|
||||
],
|
||||
});
|
||||
|
||||
setTag('server_type', serverType);
|
||||
|
||||
if (serverName) {
|
||||
setUser({ id: serverName });
|
||||
}
|
||||
|
||||
this.report = (error, options) => captureException(error, options);
|
||||
this.beforeSendFilter = beforeSendFilter;
|
||||
}
|
||||
|
||||
async beforeSend(event: ErrorEvent, hint: EventHint) {
|
||||
let { originalException } = hint;
|
||||
|
||||
if (!originalException) return null;
|
||||
|
||||
if (originalException instanceof Promise) {
|
||||
originalException = await originalException.catch((error) => error as Error);
|
||||
}
|
||||
|
||||
if (
|
||||
this.beforeSendFilter?.(event, {
|
||||
...hint,
|
||||
originalException,
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 (originalException instanceof ApplicationError || originalException instanceof BaseError) {
|
||||
if (this.isIgnoredN8nError(originalException)) return null;
|
||||
|
||||
this.extractEventDetailsFromN8nError(event, originalException);
|
||||
}
|
||||
|
||||
if (
|
||||
originalException instanceof Error &&
|
||||
'cause' in originalException &&
|
||||
originalException.cause instanceof Error &&
|
||||
'level' in originalException.cause &&
|
||||
(originalException.cause.level === 'warning' || originalException.cause.level === 'info')
|
||||
) {
|
||||
// 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) {
|
||||
if (e instanceof ExecutionCancelledError) return;
|
||||
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;
|
||||
}
|
||||
|
||||
/** @returns true if the error should be filtered out */
|
||||
private isIgnoredSqliteError(error: unknown) {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
error.name === 'QueryFailedError' &&
|
||||
typeof error.message === 'string' &&
|
||||
['SQLITE_FULL', 'SQLITE_IOERR'].some((errMsg) => error.message.includes(errMsg))
|
||||
);
|
||||
}
|
||||
|
||||
private isIgnoredN8nError(error: ApplicationError | BaseError) {
|
||||
return error.level === 'warning' || error.level === 'info';
|
||||
}
|
||||
|
||||
private extractEventDetailsFromN8nError(
|
||||
event: ErrorEvent,
|
||||
originalException: ApplicationError | BaseError,
|
||||
) {
|
||||
const { level, extra, tags } = originalException;
|
||||
event.level = level;
|
||||
if (extra) event.extra = { ...event.extra, ...extra };
|
||||
if (tags) event.tags = { ...event.tags, ...tags };
|
||||
}
|
||||
|
||||
private async getEventLoopBlockIntegration(tags: Record<string, string>, threshold?: number) {
|
||||
try {
|
||||
const { eventLoopBlockIntegration } = await import('@sentry/node-native');
|
||||
return [
|
||||
eventLoopBlockIntegration({
|
||||
...(threshold ? { threshold } : {}),
|
||||
staticTags: tags,
|
||||
}),
|
||||
];
|
||||
} catch {
|
||||
this.logger.warn(
|
||||
"Sentry's event loop block integration is disabled, because the native binary for `@sentry/node-native` was not found",
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async getProfilingIntegration() {
|
||||
try {
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
return [nodeProfilingIntegration()];
|
||||
} catch {
|
||||
this.logger.warn(
|
||||
'Sentry profiling is disabled, because the `@sentry/profiling-node` package was not found',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { FileSystemError } from './abstract/filesystem.error';
|
||||
|
||||
export class FileNotFoundError extends FileSystemError {
|
||||
constructor(filePath: string) {
|
||||
super('File not found', filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { UserError } from 'n8n-workflow';
|
||||
|
||||
export class FileTooLargeError extends UserError {
|
||||
constructor({
|
||||
fileSizeMb,
|
||||
maxFileSizeMb,
|
||||
fileId,
|
||||
fileName,
|
||||
}: {
|
||||
fileSizeMb: number;
|
||||
maxFileSizeMb: number;
|
||||
fileId: string;
|
||||
fileName?: string;
|
||||
}) {
|
||||
const id = fileName ? `"${fileName}" (${fileId})` : fileId;
|
||||
const roundedSize = Math.round(fileSizeMb * 100) / 100;
|
||||
super(
|
||||
`Failed to write binary file ${id} because its size of ${roundedSize} MB exceeds the max size limit of ${maxFileSizeMb} MB set for \`database\` mode. Consider increasing \`N8N_BINARY_DATA_DATABASE_MAX_FILE_SIZE\` up to 1 GB, or using S3 storage mode if you require writes larger than 1 GB.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export { BinaryDataFileNotFoundError } from './binary-data-file-not-found.error';
|
||||
export { FileNotFoundError } from './file-not-found.error';
|
||||
export { FileTooLargeError } from './file-too-large.error';
|
||||
export { DisallowedFilepathError } from './disallowed-filepath.error';
|
||||
export { InvalidManagerError } from './invalid-manager.error';
|
||||
export { InvalidExecutionMetadataError } from './invalid-execution-metadata.error';
|
||||
export { InvalidSourceTypeError } from './invalid-source-type.error';
|
||||
export { MissingSourceIdError } from './missing-source-id.error';
|
||||
export { UnrecognizedCredentialTypeError } from './unrecognized-credential-type.error';
|
||||
export { UnrecognizedNodeTypeError } from './unrecognized-node-type.error';
|
||||
|
||||
export { ErrorReporter } from './error-reporter';
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApplicationError } from '@n8n/errors';
|
||||
|
||||
export class InvalidExecutionMetadataError extends ApplicationError {
|
||||
constructor(
|
||||
public type: 'key' | 'value',
|
||||
key: unknown,
|
||||
message?: string,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
super(message ?? `Custom data ${type}s must be a string (key "${key}")`, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BinaryDataError } from './abstract/binary-data.error';
|
||||
|
||||
export class InvalidManagerError extends BinaryDataError {
|
||||
constructor(mode: string) {
|
||||
super(`No binary data manager found for: ${mode}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
|
||||
export class InvalidSourceTypeError extends UnexpectedError {
|
||||
constructor(sourceType: string) {
|
||||
super(`Custom file location with invalid source type: ${sourceType}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
|
||||
export class MissingSourceIdError extends UnexpectedError {
|
||||
constructor(pathSegments: string[]) {
|
||||
super(`Custom file location missing sourceId: ${pathSegments.join('/')}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { UserError } from 'n8n-workflow';
|
||||
|
||||
export class UnrecognizedCredentialTypeError extends UserError {
|
||||
constructor(credentialType: string) {
|
||||
super(`Unrecognized credential type: ${credentialType}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { UserError } from 'n8n-workflow';
|
||||
|
||||
export class UnrecognizedNodeTypeError extends UserError {
|
||||
constructor(packageName: string, nodeType: string) {
|
||||
super(`Unrecognized node type: ${packageName}.${nodeType}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { WorkflowOperationError } from 'n8n-workflow';
|
||||
|
||||
export class WorkflowHasIssuesError extends WorkflowOperationError {
|
||||
constructor() {
|
||||
super('The workflow has issues and cannot be executed for that reason. Please fix them first.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user