Rename to ErrorReporter and improve code

This commit is contained in:
Tomi Turtiainen 2024-11-12 10:45:57 +02:00
parent e795d0bae7
commit 4fceebf8c2
4 changed files with 69 additions and 37 deletions

View file

@ -35,10 +35,10 @@
}, },
"dependencies": { "dependencies": {
"@n8n/config": "workspace:*", "@n8n/config": "workspace:*",
"acorn": "8.14.0",
"acorn-walk": "8.3.4",
"@sentry/integrations": "catalog:", "@sentry/integrations": "catalog:",
"@sentry/node": "catalog:", "@sentry/node": "catalog:",
"acorn": "8.14.0",
"acorn-walk": "8.3.4",
"n8n-core": "workspace:*", "n8n-core": "workspace:*",
"n8n-workflow": "workspace:*", "n8n-workflow": "workspace:*",
"nanoid": "^3.3.6", "nanoid": "^3.3.6",

View file

@ -0,0 +1,31 @@
import { mock } from 'jest-mock-extended';
import { ApplicationError } from 'n8n-workflow';
import { ErrorReporter } from '../error-reporter';
describe('ErrorReporter', () => {
const errorReporting = new ErrorReporter(mock());
describe('beforeSend', () => {
it('should return null if originalException is an ApplicationError with level warning', () => {
const hint = { originalException: new ApplicationError('Test error', { level: 'warning' }) };
expect(errorReporting.beforeSend(mock(), hint)).toBeNull();
});
it('should return event if originalException is an ApplicationError with level error', () => {
const hint = { originalException: new ApplicationError('Test error', { level: 'error' }) };
expect(errorReporting.beforeSend(mock(), hint)).not.toBeNull();
});
it('should return null if originalException is an Error with a non-unique stack', () => {
const hint = { originalException: new Error('Test error') };
errorReporting.beforeSend(mock(), hint);
expect(errorReporting.beforeSend(mock(), hint)).toBeNull();
});
it('should return event if originalException is an Error with a unique stack', () => {
const hint = { originalException: new Error('Test error') };
expect(errorReporting.beforeSend(mock(), hint)).not.toBeNull();
});
});
});

View file

@ -1,5 +1,6 @@
import { RewriteFrames } from '@sentry/integrations'; import { RewriteFrames } from '@sentry/integrations';
import { init, setTag, captureException, close } from '@sentry/node'; import { init, setTag, captureException, close } from '@sentry/node';
import type { ErrorEvent, EventHint } from '@sentry/types';
import * as a from 'assert/strict'; import * as a from 'assert/strict';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { ApplicationError } from 'n8n-workflow'; import { ApplicationError } from 'n8n-workflow';
@ -9,9 +10,12 @@ import type { SentryConfig } from '@/config/sentry-config';
/** /**
* Handles error reporting using Sentry * Handles error reporting using Sentry
*/ */
export class ErrorReporting { export class ErrorReporter {
private isInitialized = false; private isInitialized = false;
/** Hashes of error stack traces, to deduplicate error reports. */
private readonly seenErrors = new Set<string>();
private get dsn() { private get dsn() {
return this.sentryConfig.sentryDsn; return this.sentryConfig.sentryDsn;
} }
@ -37,7 +41,8 @@ export class ErrorReporting {
'OnUnhandledRejection', 'OnUnhandledRejection',
'ContextLines', 'ContextLines',
]; ];
const seenErrors = new Set<string>();
setTag('server_type', 'task_runner');
init({ init({
dsn: this.dsn, dsn: this.dsn,
@ -46,37 +51,13 @@ export class ErrorReporting {
enableTracing: false, enableTracing: false,
serverName: this.sentryConfig.deploymentName, serverName: this.sentryConfig.deploymentName,
beforeBreadcrumb: () => null, beforeBreadcrumb: () => null,
beforeSend: (event, hint) => this.beforeSend(event, hint),
integrations: (integrations) => [ integrations: (integrations) => [
...integrations.filter(({ name }) => enabledIntegrations.includes(name)), ...integrations.filter(({ name }) => enabledIntegrations.includes(name)),
new RewriteFrames({ root: process.cwd() }), new RewriteFrames({ root: process.cwd() }),
], ],
async beforeSend(event, { originalException }) {
if (!originalException) return null;
if (originalException instanceof Promise) {
originalException = await originalException.catch((error) => error as Error);
}
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 (originalException instanceof Error && originalException.stack) {
const eventHash = createHash('sha1').update(originalException.stack).digest('base64');
if (seenErrors.has(eventHash)) return null;
seenErrors.add(eventHash);
}
return event;
},
}); });
setTag('server_type', 'task_runner');
this.isInitialized = true; this.isInitialized = true;
} }
@ -87,4 +68,24 @@ export class ErrorReporting {
await close(1000); await close(1000);
} }
beforeSend(event: ErrorEvent, { originalException }: EventHint) {
if (!originalException) 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 (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;
}
} }

View file

@ -2,12 +2,12 @@ import { ensureError } from 'n8n-workflow';
import Container from 'typedi'; import Container from 'typedi';
import { MainConfig } from './config/main-config'; import { MainConfig } from './config/main-config';
import type { ErrorReporting } from './error-reporting'; import type { ErrorReporter } from './error-reporter';
import { JsTaskRunner } from './js-task-runner/js-task-runner'; import { JsTaskRunner } from './js-task-runner/js-task-runner';
let runner: JsTaskRunner | undefined; let runner: JsTaskRunner | undefined;
let isShuttingDown = false; let isShuttingDown = false;
let errorReporting: ErrorReporting | undefined; let errorReporter: ErrorReporter | undefined;
function createSignalHandler(signal: string) { function createSignalHandler(signal: string) {
return async function onSignal() { return async function onSignal() {
@ -24,9 +24,9 @@ function createSignalHandler(signal: string) {
runner = undefined; runner = undefined;
} }
if (errorReporting) { if (errorReporter) {
await errorReporting.stop(); await errorReporter.stop();
errorReporting = undefined; errorReporter = undefined;
} }
} catch (e) { } catch (e) {
const error = ensureError(e); const error = ensureError(e);
@ -42,9 +42,9 @@ void (async function start() {
const config = Container.get(MainConfig); const config = Container.get(MainConfig);
if (config.sentryConfig.sentryDsn) { if (config.sentryConfig.sentryDsn) {
const { ErrorReporting } = await import('@/error-reporting'); const { ErrorReporter } = await import('@/error-reporter');
errorReporting = new ErrorReporting(config.sentryConfig); errorReporter = new ErrorReporter(config.sentryConfig);
await errorReporting.start(); await errorReporter.start();
} }
runner = new JsTaskRunner(config); runner = new JsTaskRunner(config);