mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-24 19:33:08 +00:00
Major improvements to telemetry system addressing code review findings: Architecture & Modularization: - Split 636-line TelemetryManager into 7 focused modules - Separated concerns: event tracking, batch processing, validation, rate limiting - Lazy initialization pattern to avoid early singleton creation - Clean separation of responsibilities Security & Privacy: - Added comprehensive input validation with Zod schemas - Sanitization of sensitive data (URLs, API keys, emails) - Expanded sensitive key detection patterns (25+ patterns) - Row Level Security on Supabase backend - Added data deletion contact info (romuald@n8n-mcp.com) Performance & Reliability: - Sliding window rate limiter (100 events/minute) - Circuit breaker pattern for network failures - Dead letter queue for failed events - Exponential backoff with jitter for retries - Performance monitoring with overhead tracking (<5%) - Memory-safe array limits in rate limiter Testing: - Comprehensive test coverage (87%+ for core modules) - Unit tests for all new modules - Integration tests for MCP telemetry - Fixed test isolation issues Data Management: - Clear user consent in welcome message - Batch processing with deduplication - Automatic workflow flushing BREAKING CHANGE: TelemetryManager constructor is now private, use getInstance() 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
173 lines
5.0 KiB
TypeScript
173 lines
5.0 KiB
TypeScript
/**
|
|
* Rate Limiter for Telemetry
|
|
* Implements sliding window rate limiting to prevent excessive telemetry events
|
|
*/
|
|
|
|
import { TELEMETRY_CONFIG } from './telemetry-types';
|
|
import { logger } from '../utils/logger';
|
|
|
|
export class TelemetryRateLimiter {
|
|
private eventTimestamps: number[] = [];
|
|
private windowMs: number;
|
|
private maxEvents: number;
|
|
private droppedEventsCount: number = 0;
|
|
private lastWarningTime: number = 0;
|
|
private readonly WARNING_INTERVAL = 60000; // Warn at most once per minute
|
|
private readonly MAX_ARRAY_SIZE = 1000; // Prevent memory leaks by limiting array size
|
|
|
|
constructor(
|
|
windowMs: number = TELEMETRY_CONFIG.RATE_LIMIT_WINDOW,
|
|
maxEvents: number = TELEMETRY_CONFIG.RATE_LIMIT_MAX_EVENTS
|
|
) {
|
|
this.windowMs = windowMs;
|
|
this.maxEvents = maxEvents;
|
|
}
|
|
|
|
/**
|
|
* Check if an event can be tracked based on rate limits
|
|
* Returns true if event can proceed, false if rate limited
|
|
*/
|
|
allow(): boolean {
|
|
const now = Date.now();
|
|
|
|
// Clean up old timestamps outside the window
|
|
this.cleanupOldTimestamps(now);
|
|
|
|
// Check if we've hit the rate limit
|
|
if (this.eventTimestamps.length >= this.maxEvents) {
|
|
this.handleRateLimitHit(now);
|
|
return false;
|
|
}
|
|
|
|
// Add current timestamp and allow event
|
|
this.eventTimestamps.push(now);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if rate limiting would occur without actually blocking
|
|
* Useful for pre-flight checks
|
|
*/
|
|
wouldAllow(): boolean {
|
|
const now = Date.now();
|
|
this.cleanupOldTimestamps(now);
|
|
return this.eventTimestamps.length < this.maxEvents;
|
|
}
|
|
|
|
/**
|
|
* Get current usage statistics
|
|
*/
|
|
getStats() {
|
|
const now = Date.now();
|
|
this.cleanupOldTimestamps(now);
|
|
|
|
return {
|
|
currentEvents: this.eventTimestamps.length,
|
|
maxEvents: this.maxEvents,
|
|
windowMs: this.windowMs,
|
|
droppedEvents: this.droppedEventsCount,
|
|
utilizationPercent: Math.round((this.eventTimestamps.length / this.maxEvents) * 100),
|
|
remainingCapacity: Math.max(0, this.maxEvents - this.eventTimestamps.length),
|
|
arraySize: this.eventTimestamps.length,
|
|
maxArraySize: this.MAX_ARRAY_SIZE,
|
|
memoryUsagePercent: Math.round((this.eventTimestamps.length / this.MAX_ARRAY_SIZE) * 100)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reset the rate limiter (useful for testing)
|
|
*/
|
|
reset(): void {
|
|
this.eventTimestamps = [];
|
|
this.droppedEventsCount = 0;
|
|
this.lastWarningTime = 0;
|
|
}
|
|
|
|
/**
|
|
* Clean up timestamps outside the current window and enforce array size limit
|
|
*/
|
|
private cleanupOldTimestamps(now: number): void {
|
|
const windowStart = now - this.windowMs;
|
|
|
|
// Remove all timestamps before the window start
|
|
let i = 0;
|
|
while (i < this.eventTimestamps.length && this.eventTimestamps[i] < windowStart) {
|
|
i++;
|
|
}
|
|
|
|
if (i > 0) {
|
|
this.eventTimestamps.splice(0, i);
|
|
}
|
|
|
|
// Enforce maximum array size to prevent memory leaks
|
|
if (this.eventTimestamps.length > this.MAX_ARRAY_SIZE) {
|
|
const excess = this.eventTimestamps.length - this.MAX_ARRAY_SIZE;
|
|
this.eventTimestamps.splice(0, excess);
|
|
|
|
if (now - this.lastWarningTime > this.WARNING_INTERVAL) {
|
|
logger.debug(
|
|
`Telemetry rate limiter array trimmed: removed ${excess} oldest timestamps to prevent memory leak. ` +
|
|
`Array size: ${this.eventTimestamps.length}/${this.MAX_ARRAY_SIZE}`
|
|
);
|
|
this.lastWarningTime = now;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle rate limit hit
|
|
*/
|
|
private handleRateLimitHit(now: number): void {
|
|
this.droppedEventsCount++;
|
|
|
|
// Log warning if enough time has passed since last warning
|
|
if (now - this.lastWarningTime > this.WARNING_INTERVAL) {
|
|
const stats = this.getStats();
|
|
logger.debug(
|
|
`Telemetry rate limit reached: ${stats.currentEvents}/${stats.maxEvents} events in ${stats.windowMs}ms window. ` +
|
|
`Total dropped: ${stats.droppedEvents}`
|
|
);
|
|
this.lastWarningTime = now;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the number of dropped events
|
|
*/
|
|
getDroppedEventsCount(): number {
|
|
return this.droppedEventsCount;
|
|
}
|
|
|
|
/**
|
|
* Estimate time until capacity is available (in ms)
|
|
* Returns 0 if capacity is available now
|
|
*/
|
|
getTimeUntilCapacity(): number {
|
|
const now = Date.now();
|
|
this.cleanupOldTimestamps(now);
|
|
|
|
if (this.eventTimestamps.length < this.maxEvents) {
|
|
return 0;
|
|
}
|
|
|
|
// Find the oldest timestamp that would need to expire
|
|
const oldestRelevant = this.eventTimestamps[this.eventTimestamps.length - this.maxEvents];
|
|
const timeUntilExpiry = Math.max(0, (oldestRelevant + this.windowMs) - now);
|
|
|
|
return timeUntilExpiry;
|
|
}
|
|
|
|
/**
|
|
* Update rate limit configuration dynamically
|
|
*/
|
|
updateLimits(windowMs?: number, maxEvents?: number): void {
|
|
if (windowMs !== undefined && windowMs > 0) {
|
|
this.windowMs = windowMs;
|
|
}
|
|
if (maxEvents !== undefined && maxEvents > 0) {
|
|
this.maxEvents = maxEvents;
|
|
}
|
|
|
|
logger.debug(`Rate limiter updated: ${this.maxEvents} events per ${this.windowMs}ms`);
|
|
}
|
|
} |