diff --git a/packages/cli/package.json b/packages/cli/package.json index 18cd195620..413bc5d700 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -94,7 +94,7 @@ "@n8n/permissions": "workspace:*", "@n8n/task-runner": "workspace:*", "@n8n/typeorm": "0.3.20-12", - "@n8n_io/ai-assistant-sdk": "1.10.3", + "@n8n_io/ai-assistant-sdk": "1.12.0", "@n8n_io/license-sdk": "2.13.1", "@oclif/core": "4.0.7", "@rudderstack/rudder-sdk-node": "2.0.9", diff --git a/packages/cli/src/__tests__/error-reporting.test.ts b/packages/cli/src/__tests__/error-reporting.test.ts new file mode 100644 index 0000000000..5e472b8b99 --- /dev/null +++ b/packages/cli/src/__tests__/error-reporting.test.ts @@ -0,0 +1,61 @@ +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/integrations'); +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); + }); +}); diff --git a/packages/cli/src/controllers/ai.controller.ts b/packages/cli/src/controllers/ai.controller.ts index 1957db2971..be1231911a 100644 --- a/packages/cli/src/controllers/ai.controller.ts +++ b/packages/cli/src/controllers/ai.controller.ts @@ -1,6 +1,5 @@ import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk'; import type { Response } from 'express'; -import { ErrorReporterProxy } from 'n8n-workflow'; import { strict as assert } from 'node:assert'; import { WritableStream } from 'node:stream/web'; @@ -33,8 +32,7 @@ export class AiController { } } catch (e) { assert(e instanceof Error); - ErrorReporterProxy.error(e); - throw new InternalServerError(`Something went wrong: ${e.message}`); + throw new InternalServerError(e.message, e); } } @@ -46,8 +44,7 @@ export class AiController { return await this.aiService.applySuggestion(req.body, req.user); } catch (e) { assert(e instanceof Error); - ErrorReporterProxy.error(e); - throw new InternalServerError(`Something went wrong: ${e.message}`); + throw new InternalServerError(e.message, e); } } @@ -57,8 +54,7 @@ export class AiController { return await this.aiService.askAi(req.body, req.user); } catch (e) { assert(e instanceof Error); - ErrorReporterProxy.error(e); - throw new InternalServerError(`Something went wrong: ${e.message}`); + throw new InternalServerError(e.message, e); } } } diff --git a/packages/cli/src/controllers/community-packages.controller.ts b/packages/cli/src/controllers/community-packages.controller.ts index 5caf835f60..918f1cdf74 100644 --- a/packages/cli/src/controllers/community-packages.controller.ts +++ b/packages/cli/src/controllers/community-packages.controller.ts @@ -201,7 +201,7 @@ export class CommunityPackagesController { error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON, ].join(':'); - throw new InternalServerError(message); + throw new InternalServerError(message, error); } // broadcast to connected frontends that node list has been updated @@ -283,7 +283,7 @@ export class CommunityPackagesController { error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON, ].join(':'); - throw new InternalServerError(message); + throw new InternalServerError(message, error); } } } diff --git a/packages/cli/src/controllers/password-reset.controller.ts b/packages/cli/src/controllers/password-reset.controller.ts index 88155e420a..a14b1f06be 100644 --- a/packages/cli/src/controllers/password-reset.controller.ts +++ b/packages/cli/src/controllers/password-reset.controller.ts @@ -120,7 +120,7 @@ export class PasswordResetController { publicApi: false, }); if (error instanceof Error) { - throw new InternalServerError(`Please contact your administrator: ${error.message}`); + throw new InternalServerError(`Please contact your administrator: ${error.message}`, error); } } diff --git a/packages/cli/src/controllers/translation.controller.ts b/packages/cli/src/controllers/translation.controller.ts index 485e290cad..d6ae656afe 100644 --- a/packages/cli/src/controllers/translation.controller.ts +++ b/packages/cli/src/controllers/translation.controller.ts @@ -54,7 +54,7 @@ export class TranslationController { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return require(NODE_HEADERS_PATH); } catch (error) { - throw new InternalServerError('Failed to load headers file'); + throw new InternalServerError('Failed to load headers file', error); } } } diff --git a/packages/cli/src/error-reporting.ts b/packages/cli/src/error-reporting.ts index 897bef6fef..fd2ce078cd 100644 --- a/packages/cli/src/error-reporting.ts +++ b/packages/cli/src/error-reporting.ts @@ -90,6 +90,17 @@ export const initErrorHandling = async () => { 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; diff --git a/packages/cli/src/errors/response-errors/abstract/response.error.ts b/packages/cli/src/errors/response-errors/abstract/response.error.ts index f756afce41..fa7d4a8b06 100644 --- a/packages/cli/src/errors/response-errors/abstract/response.error.ts +++ b/packages/cli/src/errors/response-errors/abstract/response.error.ts @@ -16,8 +16,9 @@ export abstract class ResponseError extends ApplicationError { readonly errorCode: number = httpStatusCode, // The error hint the response readonly hint: string | undefined = undefined, + cause?: unknown, ) { - super(message); + super(message, { cause }); this.name = 'ResponseError'; } } diff --git a/packages/cli/src/errors/response-errors/internal-server.error.ts b/packages/cli/src/errors/response-errors/internal-server.error.ts index 4c10e93f95..2a6a8d6b77 100644 --- a/packages/cli/src/errors/response-errors/internal-server.error.ts +++ b/packages/cli/src/errors/response-errors/internal-server.error.ts @@ -1,7 +1,7 @@ import { ResponseError } from './abstract/response.error'; export class InternalServerError extends ResponseError { - constructor(message: string, errorCode = 500) { - super(message, 500, errorCode); + constructor(message: string, cause?: unknown) { + super(message, 500, 500, undefined, cause); } } diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index 60dadfdc1b..67eb145b19 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -251,7 +251,7 @@ export class ExecutionService { requestFilters = requestFiltersRaw as IGetExecutionsQueryFilter; } } catch (error) { - throw new InternalServerError('Parameter "filter" contained invalid JSON string.'); + throw new InternalServerError('Parameter "filter" contained invalid JSON string.', error); } } diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index ba02375aba..e47dd026b0 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -1,5 +1,5 @@ import type { IUserSettings } from 'n8n-workflow'; -import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; +import { ApplicationError } from 'n8n-workflow'; import { Service } from 'typedi'; import type { User, AssignableRole } from '@/databases/entities/user'; @@ -213,9 +213,8 @@ export class UserService { ), ); } catch (error) { - ErrorReporter.error(error); this.logger.error('Failed to create user shells', { userShells: createdUsers }); - throw new InternalServerError('An error occurred during user creation'); + throw new InternalServerError('An error occurred during user creation', error); } pendingUsersToInvite.forEach(({ email, id }) => createdUsers.set(email, id)); diff --git a/packages/cli/src/user-management/email/user-management-mailer.ts b/packages/cli/src/user-management/email/user-management-mailer.ts index b5df958d7d..3acddad185 100644 --- a/packages/cli/src/user-management/email/user-management-mailer.ts +++ b/packages/cli/src/user-management/email/user-management-mailer.ts @@ -125,7 +125,7 @@ export class UserManagementMailer { const error = toError(e); - throw new InternalServerError(`Please contact your administrator: ${error.message}`); + throw new InternalServerError(`Please contact your administrator: ${error.message}`, e); } } @@ -180,7 +180,7 @@ export class UserManagementMailer { const error = toError(e); - throw new InternalServerError(`Please contact your administrator: ${error.message}`); + throw new InternalServerError(`Please contact your administrator: ${error.message}`, e); } } diff --git a/packages/cli/src/webhooks/webhook-helpers.ts b/packages/cli/src/webhooks/webhook-helpers.ts index 6110584f7e..0dd8f576a4 100644 --- a/packages/cli/src/webhooks/webhook-helpers.ts +++ b/packages/cli/src/webhooks/webhook-helpers.ts @@ -762,7 +762,7 @@ export async function executeWebhook( ); } - const internalServerError = new InternalServerError(e.message); + const internalServerError = new InternalServerError(e.message, e); if (e instanceof ExecutionCancelledError) internalServerError.level = 'warning'; throw internalServerError; }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6aa8e13bc..f121c953fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -776,8 +776,8 @@ importers: specifier: 0.3.20-12 version: 0.3.20-12(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.7.2)) '@n8n_io/ai-assistant-sdk': - specifier: 1.10.3 - version: 1.10.3 + specifier: 1.12.0 + version: 1.12.0 '@n8n_io/license-sdk': specifier: 2.13.1 version: 2.13.1 @@ -3795,8 +3795,8 @@ packages: engines: {node: '>=18.10', pnpm: '>=9.6'} hasBin: true - '@n8n_io/ai-assistant-sdk@1.10.3': - resolution: {integrity: sha512-oHuYryPoWa+KcIf7ihrlXSVpi3XhVPmZX4tUX4TMvgAeipu7yhf7ZSXQFeBUx6zD1oa2zOXIs4b0NsXELrZWqg==} + '@n8n_io/ai-assistant-sdk@1.12.0': + resolution: {integrity: sha512-ddIGzUn8icxWwl49PLSpl/Gfb0bCIGpqvWtZWqC3GIBeb51Nul6E4e3cIyDYOFlZmWWr/BDKsN0wskm2s/jkdg==} engines: {node: '>=20.15', pnpm: '>=8.14'} '@n8n_io/license-sdk@2.13.1': @@ -15023,7 +15023,7 @@ snapshots: acorn: 8.12.1 acorn-walk: 8.3.4 - '@n8n_io/ai-assistant-sdk@1.10.3': {} + '@n8n_io/ai-assistant-sdk@1.12.0': {} '@n8n_io/license-sdk@2.13.1': dependencies: