mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(core): Filter out task runner errors originating from user code from sentry (no-changelog) (#12537)
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import type { ErrorEvent } from '@sentry/types';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { ErrorReporter } from 'n8n-core';
|
||||
|
||||
import { TaskRunnerSentry } from '../task-runner-sentry';
|
||||
|
||||
describe('TaskRunnerSentry', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('filterOutUserCodeErrors', () => {
|
||||
const sentry = new TaskRunnerSentry(
|
||||
{
|
||||
dsn: 'https://sentry.io/123',
|
||||
n8nVersion: '1.0.0',
|
||||
environment: 'local',
|
||||
deploymentName: 'test',
|
||||
},
|
||||
mock(),
|
||||
);
|
||||
|
||||
it('should filter out user code errors', () => {
|
||||
const event: ErrorEvent = {
|
||||
type: undefined,
|
||||
exception: {
|
||||
values: [
|
||||
{
|
||||
type: 'ReferenceError',
|
||||
value: 'fetch is not defined',
|
||||
stacktrace: {
|
||||
frames: [
|
||||
{
|
||||
filename: 'app:///dist/js-task-runner/js-task-runner.js',
|
||||
module: 'js-task-runner:js-task-runner',
|
||||
function: 'JsTaskRunner.executeTask',
|
||||
},
|
||||
{
|
||||
filename: 'app:///dist/js-task-runner/js-task-runner.js',
|
||||
module: 'js-task-runner:js-task-runner',
|
||||
function: 'JsTaskRunner.runForAllItems',
|
||||
},
|
||||
{
|
||||
filename: '<anonymous>',
|
||||
module: '<anonymous>',
|
||||
function: 'new Promise',
|
||||
},
|
||||
{
|
||||
filename: 'app:///dist/js-task-runner/js-task-runner.js',
|
||||
module: 'js-task-runner:js-task-runner',
|
||||
function: 'result',
|
||||
},
|
||||
{
|
||||
filename: 'node:vm',
|
||||
module: 'node:vm',
|
||||
function: 'runInContext',
|
||||
},
|
||||
{
|
||||
filename: 'node:vm',
|
||||
module: 'node:vm',
|
||||
function: 'Script.runInContext',
|
||||
},
|
||||
{
|
||||
filename: 'evalmachine.<anonymous>',
|
||||
module: 'evalmachine.<anonymous>',
|
||||
function: '?',
|
||||
},
|
||||
{
|
||||
filename: 'evalmachine.<anonymous>',
|
||||
module: 'evalmachine.<anonymous>',
|
||||
function: 'VmCodeWrapper',
|
||||
},
|
||||
{
|
||||
filename: '<anonymous>',
|
||||
module: '<anonymous>',
|
||||
function: 'new Promise',
|
||||
},
|
||||
{
|
||||
filename: 'evalmachine.<anonymous>',
|
||||
module: 'evalmachine.<anonymous>',
|
||||
},
|
||||
],
|
||||
},
|
||||
mechanism: { type: 'onunhandledrejection', handled: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
event_id: '18bb78bb3d9d44c4acf3d774c2cfbfd8',
|
||||
platform: 'node',
|
||||
contexts: {
|
||||
trace: { trace_id: '3c3614d33a6b47f09b85ec7d2710acea', span_id: 'ad00fdf6d6173aeb' },
|
||||
runtime: { name: 'node', version: 'v20.17.0' },
|
||||
},
|
||||
};
|
||||
|
||||
expect(sentry.filterOutUserCodeErrors(event)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initIfEnabled', () => {
|
||||
const mockErrorReporter = mock<ErrorReporter>();
|
||||
|
||||
it('should not configure sentry if dsn is not set', async () => {
|
||||
const sentry = new TaskRunnerSentry(
|
||||
{
|
||||
dsn: '',
|
||||
n8nVersion: '1.0.0',
|
||||
environment: 'local',
|
||||
deploymentName: 'test',
|
||||
},
|
||||
mockErrorReporter,
|
||||
);
|
||||
|
||||
await sentry.initIfEnabled();
|
||||
|
||||
expect(mockErrorReporter.init).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should configure sentry if dsn is set', async () => {
|
||||
const sentry = new TaskRunnerSentry(
|
||||
{
|
||||
dsn: 'https://sentry.io/123',
|
||||
n8nVersion: '1.0.0',
|
||||
environment: 'local',
|
||||
deploymentName: 'test',
|
||||
},
|
||||
mockErrorReporter,
|
||||
);
|
||||
|
||||
await sentry.initIfEnabled();
|
||||
|
||||
expect(mockErrorReporter.init).toHaveBeenCalledWith({
|
||||
dsn: 'https://sentry.io/123',
|
||||
beforeSendFilter: sentry.filterOutUserCodeErrors,
|
||||
release: '1.0.0',
|
||||
environment: 'local',
|
||||
serverName: 'test',
|
||||
serverType: 'task_runner',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shutdown', () => {
|
||||
const mockErrorReporter = mock<ErrorReporter>();
|
||||
|
||||
it('should not shutdown sentry if dsn is not set', async () => {
|
||||
const sentry = new TaskRunnerSentry(
|
||||
{
|
||||
dsn: '',
|
||||
n8nVersion: '1.0.0',
|
||||
environment: 'local',
|
||||
deploymentName: 'test',
|
||||
},
|
||||
mockErrorReporter,
|
||||
);
|
||||
|
||||
await sentry.shutdown();
|
||||
|
||||
expect(mockErrorReporter.shutdown).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should shutdown sentry if dsn is set', async () => {
|
||||
const sentry = new TaskRunnerSentry(
|
||||
{
|
||||
dsn: 'https://sentry.io/123',
|
||||
n8nVersion: '1.0.0',
|
||||
environment: 'local',
|
||||
deploymentName: 'test',
|
||||
},
|
||||
mockErrorReporter,
|
||||
);
|
||||
|
||||
await sentry.shutdown();
|
||||
|
||||
expect(mockErrorReporter.shutdown).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,16 @@
|
||||
import './polyfills';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { ErrorReporter } from 'n8n-core';
|
||||
import { ensureError, setGlobalState } from 'n8n-workflow';
|
||||
|
||||
import { MainConfig } from './config/main-config';
|
||||
import type { HealthCheckServer } from './health-check-server';
|
||||
import { JsTaskRunner } from './js-task-runner/js-task-runner';
|
||||
import { TaskRunnerSentry } from './task-runner-sentry';
|
||||
|
||||
let healthCheckServer: HealthCheckServer | undefined;
|
||||
let runner: JsTaskRunner | undefined;
|
||||
let isShuttingDown = false;
|
||||
let errorReporter: ErrorReporter | undefined;
|
||||
let sentry: TaskRunnerSentry | undefined;
|
||||
|
||||
function createSignalHandler(signal: string, timeoutInS = 10) {
|
||||
return async function onSignal() {
|
||||
@@ -33,9 +33,9 @@ function createSignalHandler(signal: string, timeoutInS = 10) {
|
||||
void healthCheckServer?.stop();
|
||||
}
|
||||
|
||||
if (errorReporter) {
|
||||
await errorReporter.shutdown();
|
||||
errorReporter = undefined;
|
||||
if (sentry) {
|
||||
await sentry.shutdown();
|
||||
sentry = undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
const error = ensureError(e);
|
||||
@@ -54,20 +54,8 @@ void (async function start() {
|
||||
defaultTimezone: config.baseRunnerConfig.timezone,
|
||||
});
|
||||
|
||||
const { dsn } = config.sentryConfig;
|
||||
|
||||
if (dsn) {
|
||||
const { ErrorReporter } = await import('n8n-core');
|
||||
errorReporter = Container.get(ErrorReporter);
|
||||
const { deploymentName, environment, n8nVersion } = config.sentryConfig;
|
||||
await errorReporter.init({
|
||||
serverType: 'task_runner',
|
||||
dsn,
|
||||
serverName: deploymentName,
|
||||
environment,
|
||||
release: n8nVersion,
|
||||
});
|
||||
}
|
||||
sentry = Container.get(TaskRunnerSentry);
|
||||
await sentry.initIfEnabled();
|
||||
|
||||
runner = new JsTaskRunner(config);
|
||||
runner.on('runner:reached-idle-timeout', () => {
|
||||
|
||||
62
packages/@n8n/task-runner/src/task-runner-sentry.ts
Normal file
62
packages/@n8n/task-runner/src/task-runner-sentry.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Service } from '@n8n/di';
|
||||
import type { ErrorEvent, Exception } from '@sentry/types';
|
||||
import { ErrorReporter } from 'n8n-core';
|
||||
|
||||
import { SentryConfig } from './config/sentry-config';
|
||||
|
||||
/**
|
||||
* Sentry service for the task runner.
|
||||
*/
|
||||
@Service()
|
||||
export class TaskRunnerSentry {
|
||||
constructor(
|
||||
private readonly config: SentryConfig,
|
||||
private readonly errorReporter: ErrorReporter,
|
||||
) {}
|
||||
|
||||
async initIfEnabled() {
|
||||
const { dsn, n8nVersion, environment, deploymentName } = this.config;
|
||||
|
||||
if (!dsn) return;
|
||||
|
||||
await this.errorReporter.init({
|
||||
serverType: 'task_runner',
|
||||
dsn,
|
||||
release: n8nVersion,
|
||||
environment,
|
||||
serverName: deploymentName,
|
||||
beforeSendFilter: this.filterOutUserCodeErrors,
|
||||
});
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
if (!this.config.dsn) return;
|
||||
|
||||
await this.errorReporter.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out errors originating from user provided code.
|
||||
* It is possible for users to create code that causes unhandledrejections
|
||||
* that end up in the sentry error reporting.
|
||||
*/
|
||||
filterOutUserCodeErrors = (event: ErrorEvent) => {
|
||||
const error = event?.exception?.values?.[0];
|
||||
|
||||
return error ? this.isUserCodeError(error) : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the error is originating from user provided code.
|
||||
* It is possible for users to create code that causes unhandledrejections
|
||||
* that end up in the sentry error reporting.
|
||||
*/
|
||||
private isUserCodeError(error: Exception) {
|
||||
const frames = error.stacktrace?.frames;
|
||||
if (!frames) return false;
|
||||
|
||||
return frames.some(
|
||||
(frame) => frame.filename === 'node:vm' && frame.function === 'runInContext',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user