diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index 9d84b9d1e3..00971d71a5 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -252,6 +252,7 @@ export class Server extends AbstractServer { JSON.stringify({ dsn: this.globalConfig.sentry.frontendDsn, environment: process.env.ENVIRONMENT || 'development', + serverName: process.env.DEPLOYMENT_NAME, release: N8N_VERSION, }), ); diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index d5ee658d51..b137d3eb0d 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -1,5 +1,4 @@ import { createApp } from 'vue'; -import * as Sentry from '@sentry/vue'; import '@vue-flow/core/dist/style.css'; import '@vue-flow/core/dist/theme-default.css'; @@ -30,32 +29,13 @@ import { FontAwesomePlugin } from './plugins/icons'; import { createPinia, PiniaVuePlugin } from 'pinia'; import { JsPlumbPlugin } from '@/plugins/jsplumb'; import { ChartJSPlugin } from '@/plugins/chartjs'; -import { AxiosError } from 'axios'; +import { SentryPlugin } from '@/plugins/sentry'; const pinia = createPinia(); const app = createApp(App); -if (window.sentry?.dsn) { - const { dsn, release, environment } = window.sentry; - Sentry.init({ - app, - dsn, - release, - environment, - beforeSend(event, { originalException }) { - if ( - !originalException || - originalException instanceof AxiosError || - (originalException instanceof Error && originalException.message.includes('ResizeObserver')) - ) { - return null; - } - return event; - }, - }); -} - +app.use(SentryPlugin); app.use(TelemetryPlugin); app.use(PiniaVuePlugin); app.use(I18nPlugin); diff --git a/packages/editor-ui/src/plugins/sentry.spec.ts b/packages/editor-ui/src/plugins/sentry.spec.ts new file mode 100644 index 0000000000..b8425647a2 --- /dev/null +++ b/packages/editor-ui/src/plugins/sentry.spec.ts @@ -0,0 +1,46 @@ +import type * as Sentry from '@sentry/vue'; +import { beforeSend } from '@/plugins/sentry'; +import { AxiosError } from 'axios'; +import { ResponseError } from '@/utils/apiUtils'; + +function createErrorEvent(): Sentry.ErrorEvent { + return {} as Sentry.ErrorEvent; +} + +describe('beforeSend', () => { + it('should return null when originalException is undefined', () => { + const event = createErrorEvent(); + const hint = { originalException: undefined }; + expect(beforeSend(event, hint)).toBeNull(); + }); + + it('should return null when originalException matches ignoredErrors by instance and message', () => { + const event = createErrorEvent(); + const hint = { originalException: new ResponseError("Can't connect to n8n.") }; + expect(beforeSend(event, hint)).toBeNull(); + }); + + it('should return null when originalException matches ignoredErrors by instance and message regex', () => { + const event = createErrorEvent(); + const hint = { originalException: new ResponseError('ECONNREFUSED') }; + expect(beforeSend(event, hint)).toBeNull(); + }); + + it('should return null when originalException matches ignoredErrors by instance only', () => { + const event = createErrorEvent(); + const hint = { originalException: new AxiosError() }; + expect(beforeSend(event, hint)).toBeNull(); + }); + + it('should return null when originalException matches ignoredErrors by instance and message regex (ResizeObserver)', () => { + const event = createErrorEvent(); + const hint = { originalException: new Error('ResizeObserver loop limit exceeded') }; + expect(beforeSend(event, hint)).toBeNull(); + }); + + it('should return event when originalException does not match any ignoredErrors', () => { + const event = createErrorEvent(); + const hint = { originalException: new Error('Some other error') }; + expect(beforeSend(event, hint)).toEqual(event); + }); +}); diff --git a/packages/editor-ui/src/plugins/sentry.ts b/packages/editor-ui/src/plugins/sentry.ts new file mode 100644 index 0000000000..c00826a092 --- /dev/null +++ b/packages/editor-ui/src/plugins/sentry.ts @@ -0,0 +1,59 @@ +import type { Plugin } from 'vue'; +import { AxiosError } from 'axios'; +import { ResponseError } from '@/utils/apiUtils'; +import * as Sentry from '@sentry/vue'; + +const ignoredErrors = [ + { instanceof: AxiosError }, + { instanceof: ResponseError, message: /ECONNREFUSED/ }, + { instanceof: ResponseError, message: "Can't connect to n8n." }, + { instanceof: Error, message: /ResizeObserver/ }, +] as const; + +export function beforeSend(event: Sentry.ErrorEvent, { originalException }: Sentry.EventHint) { + if ( + !originalException || + ignoredErrors.some((entry) => { + const typeMatch = originalException instanceof entry.instanceof; + if (!typeMatch) { + return false; + } + + if ('message' in entry) { + if (entry.message instanceof RegExp) { + return entry.message.test(originalException.message ?? ''); + } else { + return originalException.message === entry.message; + } + } + + return true; + }) + ) { + return null; + } + + return event; +} + +export const SentryPlugin: Plugin = { + install: (app) => { + if (!window.sentry?.dsn) { + return; + } + + const { dsn, release, environment, serverName } = window.sentry; + + Sentry.init({ + app, + dsn, + release, + environment, + beforeSend, + }); + + if (serverName) { + Sentry.setTag('server_name', serverName); + } + }, +}; diff --git a/packages/editor-ui/src/shims.d.ts b/packages/editor-ui/src/shims.d.ts index 83cd8850cd..30b8303cd8 100644 --- a/packages/editor-ui/src/shims.d.ts +++ b/packages/editor-ui/src/shims.d.ts @@ -18,7 +18,7 @@ declare global { interface Window { BASE_PATH: string; REST_ENDPOINT: string; - sentry?: { dsn?: string; environment: string; release: string }; + sentry?: { dsn?: string; environment: string; release: string; serverName?: string }; n8nExternalHooks?: PartialDeep; preventNodeViewBeforeUnload?: boolean; maxPinnedDataSize?: number;