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": {
"@n8n/config": "workspace:*",
"acorn": "8.14.0",
"acorn-walk": "8.3.4",
"@sentry/integrations": "catalog:",
"@sentry/node": "catalog:",
"acorn": "8.14.0",
"acorn-walk": "8.3.4",
"n8n-core": "workspace:*",
"n8n-workflow": "workspace:*",
"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 { init, setTag, captureException, close } from '@sentry/node';
import type { ErrorEvent, EventHint } from '@sentry/types';
import * as a from 'assert/strict';
import { createHash } from 'crypto';
import { ApplicationError } from 'n8n-workflow';
@ -9,9 +10,12 @@ import type { SentryConfig } from '@/config/sentry-config';
/**
* Handles error reporting using Sentry
*/
export class ErrorReporting {
export class ErrorReporter {
private isInitialized = false;
/** Hashes of error stack traces, to deduplicate error reports. */
private readonly seenErrors = new Set<string>();
private get dsn() {
return this.sentryConfig.sentryDsn;
}
@ -37,7 +41,8 @@ export class ErrorReporting {
'OnUnhandledRejection',
'ContextLines',
];
const seenErrors = new Set<string>();
setTag('server_type', 'task_runner');
init({
dsn: this.dsn,
@ -46,37 +51,13 @@ export class ErrorReporting {
enableTracing: false,
serverName: this.sentryConfig.deploymentName,
beforeBreadcrumb: () => null,
beforeSend: (event, hint) => this.beforeSend(event, hint),
integrations: (integrations) => [
...integrations.filter(({ name }) => enabledIntegrations.includes(name)),
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;
}
@ -87,4 +68,24 @@ export class ErrorReporting {
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 { 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';
let runner: JsTaskRunner | undefined;
let isShuttingDown = false;
let errorReporting: ErrorReporting | undefined;
let errorReporter: ErrorReporter | undefined;
function createSignalHandler(signal: string) {
return async function onSignal() {
@ -24,9 +24,9 @@ function createSignalHandler(signal: string) {
runner = undefined;
}
if (errorReporting) {
await errorReporting.stop();
errorReporting = undefined;
if (errorReporter) {
await errorReporter.stop();
errorReporter = undefined;
}
} catch (e) {
const error = ensureError(e);
@ -42,9 +42,9 @@ void (async function start() {
const config = Container.get(MainConfig);
if (config.sentryConfig.sentryDsn) {
const { ErrorReporting } = await import('@/error-reporting');
errorReporting = new ErrorReporting(config.sentryConfig);
await errorReporting.start();
const { ErrorReporter } = await import('@/error-reporter');
errorReporter = new ErrorReporter(config.sentryConfig);
await errorReporter.start();
}
runner = new JsTaskRunner(config);