mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-28 23:37:00 +02:00
feat(core): Add EventLoopBlocked rate cap to bound Sentry noise volume (#30485)
Some checks failed
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.15.0) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (26.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Build: Benchmark Image / build (push) Has been cancelled
Util: Sync API Docs / sync-public-api (push) Has been cancelled
Some checks failed
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.15.0) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (26.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Build: Benchmark Image / build (push) Has been cancelled
Util: Sync API Docs / sync-public-api (push) Has been cancelled
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c638e91aec
commit
4e1741834a
|
|
@ -45,6 +45,13 @@ export class SentryConfig {
|
|||
@Env('N8N_SENTRY_EVENT_LOOP_BLOCK_THRESHOLD', z.number({ coerce: true }).int().positive())
|
||||
eventLoopBlockThreshold: number = 500;
|
||||
|
||||
/** Leaky-bucket cap on event loop block events reported per hour per instance. @default 5 */
|
||||
@Env(
|
||||
'N8N_SENTRY_EVENT_LOOP_BLOCK_MAX_EVENTS_PER_HOUR',
|
||||
z.number({ coerce: true }).int().positive(),
|
||||
)
|
||||
eventLoopBlockMaxEventsPerHour: number = 5;
|
||||
|
||||
/**
|
||||
* Environment of the n8n instance.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -352,6 +352,7 @@ describe('GlobalConfig', () => {
|
|||
profilesSampleRate: 0,
|
||||
tracesSampleRate: 0,
|
||||
eventLoopBlockThreshold: 500,
|
||||
eventLoopBlockMaxEventsPerHour: 5,
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ export abstract class BaseCommand<F = never> {
|
|||
profilesSampleRate,
|
||||
tracesSampleRate,
|
||||
eventLoopBlockThreshold,
|
||||
eventLoopBlockMaxEventsPerHour,
|
||||
} = this.globalConfig.sentry;
|
||||
await this.errorReporter.init({
|
||||
serverType: this.instanceSettings.instanceType,
|
||||
|
|
@ -103,6 +104,7 @@ export abstract class BaseCommand<F = never> {
|
|||
releaseDate: N8N_RELEASE_DATE,
|
||||
withEventLoopBlockDetection: true,
|
||||
eventLoopBlockThreshold,
|
||||
eventLoopBlockMaxEventsPerHour,
|
||||
tracesSampleRate,
|
||||
profilesSampleRate,
|
||||
healthEndpoint: resolveBackendHealthEndpointPath(this.globalConfig),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ jest.mock('@sentry/node', () => ({
|
|||
Integrations: {},
|
||||
}));
|
||||
|
||||
const eventLoopBlockIntegrationMock = jest.fn((opts: unknown) => ({
|
||||
name: 'EventLoopBlock',
|
||||
opts,
|
||||
}));
|
||||
jest.mock('@sentry/node-native', () => ({
|
||||
eventLoopBlockIntegration: (opts: unknown) => eventLoopBlockIntegrationMock(opts),
|
||||
}));
|
||||
|
||||
jest.spyOn(process, 'on');
|
||||
|
||||
describe('ErrorReporter', () => {
|
||||
|
|
@ -171,6 +179,42 @@ describe('ErrorReporter', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getEventLoopBlockIntegration', () => {
|
||||
const tags = { server_name: 'test', server_type: 'main' as const };
|
||||
|
||||
beforeEach(() => {
|
||||
eventLoopBlockIntegrationMock.mockClear();
|
||||
});
|
||||
|
||||
it('passes threshold and maxEventsPerHour through to the Sentry integration', async () => {
|
||||
// @ts-expect-error - private method
|
||||
await errorReporter.getEventLoopBlockIntegration(tags, 750, 3);
|
||||
|
||||
expect(eventLoopBlockIntegrationMock).toHaveBeenCalledWith({
|
||||
threshold: 750,
|
||||
maxEventsPerHour: 3,
|
||||
staticTags: tags,
|
||||
});
|
||||
});
|
||||
|
||||
it('omits maxEventsPerHour when not provided (back-compat)', async () => {
|
||||
// @ts-expect-error - private method
|
||||
await errorReporter.getEventLoopBlockIntegration(tags, 500);
|
||||
|
||||
expect(eventLoopBlockIntegrationMock).toHaveBeenCalledWith({
|
||||
threshold: 500,
|
||||
staticTags: tags,
|
||||
});
|
||||
});
|
||||
|
||||
it('omits both options when neither is provided', async () => {
|
||||
// @ts-expect-error - private method
|
||||
await errorReporter.getEventLoopBlockIntegration(tags);
|
||||
|
||||
expect(eventLoopBlockIntegrationMock).toHaveBeenCalledWith({ staticTags: tags });
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
let error: ApplicationError;
|
||||
let logger: Logger;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ type ErrorReporterInitOptions = {
|
|||
/** Threshold in ms for event loop block detection. Only used if `withEventLoopBlockDetection` is true. */
|
||||
eventLoopBlockThreshold?: number;
|
||||
|
||||
/** Max event loop block events per hour per instance. Only used if `withEventLoopBlockDetection` is true. */
|
||||
eventLoopBlockMaxEventsPerHour?: number;
|
||||
|
||||
/** Sample rate for Sentry traces (0.0 to 1.0). 0 means disabled */
|
||||
tracesSampleRate: number;
|
||||
|
||||
|
|
@ -116,6 +119,7 @@ export class ErrorReporter {
|
|||
releaseDate,
|
||||
withEventLoopBlockDetection,
|
||||
eventLoopBlockThreshold,
|
||||
eventLoopBlockMaxEventsPerHour,
|
||||
profilesSampleRate,
|
||||
tracesSampleRate,
|
||||
eligibleIntegrations = {},
|
||||
|
|
@ -196,6 +200,7 @@ export class ErrorReporter {
|
|||
server_type: serverType,
|
||||
},
|
||||
eventLoopBlockThreshold,
|
||||
eventLoopBlockMaxEventsPerHour,
|
||||
)
|
||||
: [];
|
||||
|
||||
|
|
@ -335,12 +340,17 @@ export class ErrorReporter {
|
|||
if (tags) event.tags = { ...event.tags, ...tags };
|
||||
}
|
||||
|
||||
private async getEventLoopBlockIntegration(tags: Record<string, string>, threshold?: number) {
|
||||
private async getEventLoopBlockIntegration(
|
||||
tags: Record<string, string>,
|
||||
threshold?: number,
|
||||
maxEventsPerHour?: number,
|
||||
) {
|
||||
try {
|
||||
const { eventLoopBlockIntegration } = await import('@sentry/node-native');
|
||||
return [
|
||||
eventLoopBlockIntegration({
|
||||
...(threshold ? { threshold } : {}),
|
||||
...(maxEventsPerHour ? { maxEventsPerHour } : {}),
|
||||
staticTags: tags,
|
||||
}),
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user