Skip to main content
Glama

n8n-MCP

by 88-888
early-error-logger.tsβ€’9.37 kB
/** * Early Error Logger (v2.18.3) * Captures errors that occur BEFORE the main telemetry system is ready * Uses direct Supabase insert to bypass batching and ensure immediate persistence * * CRITICAL FIXES: * - Singleton pattern to prevent multiple instances * - Defensive initialization (safe defaults before any throwing operation) * - Timeout wrapper for Supabase operations (5s max) * - Shared sanitization utilities (DRY principle) */ import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { TelemetryConfigManager } from './config-manager'; import { TELEMETRY_BACKEND } from './telemetry-types'; import { StartupCheckpoint, isValidCheckpoint, getCheckpointDescription } from './startup-checkpoints'; import { sanitizeErrorMessageCore } from './error-sanitization-utils'; import { logger } from '../utils/logger'; /** * Timeout wrapper for async operations * Prevents hanging if Supabase is unreachable */ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, operation: string): Promise<T | null> { try { const timeoutPromise = new Promise<T>((_, reject) => { setTimeout(() => reject(new Error(`${operation} timeout after ${timeoutMs}ms`)), timeoutMs); }); return await Promise.race([promise, timeoutPromise]); } catch (error) { logger.debug(`${operation} failed or timed out:`, error); return null; } } export class EarlyErrorLogger { // Singleton instance private static instance: EarlyErrorLogger | null = null; // DEFENSIVE INITIALIZATION: Initialize all fields to safe defaults FIRST // This ensures the object is in a valid state even if initialization fails private enabled: boolean = false; // Safe default: disabled private supabase: SupabaseClient | null = null; // Safe default: null private userId: string | null = null; // Safe default: null private checkpoints: StartupCheckpoint[] = []; private startTime: number = Date.now(); private initPromise: Promise<void>; /** * Private constructor - use getInstance() instead * Ensures only one instance exists per process */ private constructor() { // Kick off async initialization without blocking this.initPromise = this.initialize(); } /** * Get singleton instance * Safe to call from anywhere - initialization errors won't crash caller */ static getInstance(): EarlyErrorLogger { if (!EarlyErrorLogger.instance) { EarlyErrorLogger.instance = new EarlyErrorLogger(); } return EarlyErrorLogger.instance; } /** * Async initialization logic * Separated from constructor to prevent throwing before safe defaults are set */ private async initialize(): Promise<void> { try { // Validate backend configuration before using if (!TELEMETRY_BACKEND.URL || !TELEMETRY_BACKEND.ANON_KEY) { logger.debug('Telemetry backend not configured, early error logger disabled'); this.enabled = false; return; } // Check if telemetry is disabled by user const configManager = TelemetryConfigManager.getInstance(); const isEnabled = configManager.isEnabled(); if (!isEnabled) { logger.debug('Telemetry disabled by user, early error logger will not send events'); this.enabled = false; return; } // Initialize Supabase client for direct inserts this.supabase = createClient( TELEMETRY_BACKEND.URL, TELEMETRY_BACKEND.ANON_KEY, { auth: { persistSession: false, autoRefreshToken: false, }, } ); // Get user ID from config manager this.userId = configManager.getUserId(); // Mark as enabled only after successful initialization this.enabled = true; logger.debug('Early error logger initialized successfully'); } catch (error) { // Initialization failed - ensure safe state logger.debug('Early error logger initialization failed:', error); this.enabled = false; this.supabase = null; this.userId = null; } } /** * Wait for initialization to complete (for testing) * Not needed in production - all methods handle uninitialized state gracefully */ async waitForInit(): Promise<void> { await this.initPromise; } /** * Log a checkpoint as the server progresses through startup * FIRE-AND-FORGET: Does not block caller (no await needed) */ logCheckpoint(checkpoint: StartupCheckpoint): void { if (!this.enabled) { return; } try { // Validate checkpoint if (!isValidCheckpoint(checkpoint)) { logger.warn(`Invalid checkpoint: ${checkpoint}`); return; } // Add to internal checkpoint list this.checkpoints.push(checkpoint); logger.debug(`Checkpoint passed: ${checkpoint} (${getCheckpointDescription(checkpoint)})`); } catch (error) { // Don't throw - we don't want checkpoint logging to crash the server logger.debug('Failed to log checkpoint:', error); } } /** * Log a startup error with checkpoint context * This is the main error capture mechanism * FIRE-AND-FORGET: Does not block caller */ logStartupError(checkpoint: StartupCheckpoint, error: unknown): void { if (!this.enabled || !this.supabase || !this.userId) { return; } // Run async operation without blocking caller this.logStartupErrorAsync(checkpoint, error).catch((logError) => { // Swallow errors - telemetry must never crash the server logger.debug('Failed to log startup error:', logError); }); } /** * Internal async implementation with timeout wrapper */ private async logStartupErrorAsync(checkpoint: StartupCheckpoint, error: unknown): Promise<void> { try { // Sanitize error message using shared utilities (v2.18.3) let errorMessage = 'Unknown error'; if (error instanceof Error) { errorMessage = error.message; if (error.stack) { errorMessage = error.stack; } } else if (typeof error === 'string') { errorMessage = error; } else { errorMessage = String(error); } const sanitizedError = sanitizeErrorMessageCore(errorMessage); // Extract error type if it's an Error object let errorType = 'unknown'; if (error instanceof Error) { errorType = error.name || 'Error'; } else if (typeof error === 'string') { errorType = 'string_error'; } // Create startup_error event const event = { user_id: this.userId!, event: 'startup_error', properties: { checkpoint, errorMessage: sanitizedError, errorType, checkpointsPassed: this.checkpoints, checkpointsPassedCount: this.checkpoints.length, startupDuration: Date.now() - this.startTime, platform: process.platform, arch: process.arch, nodeVersion: process.version, isDocker: process.env.IS_DOCKER === 'true', }, created_at: new Date().toISOString(), }; // Direct insert to Supabase with timeout (5s max) const insertOperation = async () => { return await this.supabase! .from('events') .insert(event) .select() .single(); }; const result = await withTimeout(insertOperation(), 5000, 'Startup error insert'); if (result && 'error' in result && result.error) { logger.debug('Failed to insert startup error event:', result.error); } else if (result) { logger.debug(`Startup error logged for checkpoint: ${checkpoint}`); } } catch (logError) { // Don't throw - telemetry failures should never crash the server logger.debug('Failed to log startup error:', logError); } } /** * Log successful startup completion * Called when all checkpoints have been passed * FIRE-AND-FORGET: Does not block caller */ logStartupSuccess(checkpoints: StartupCheckpoint[], durationMs: number): void { if (!this.enabled) { return; } try { // Store checkpoints for potential session_start enhancement this.checkpoints = checkpoints; logger.debug(`Startup successful: ${checkpoints.length} checkpoints passed in ${durationMs}ms`); // We don't send a separate event here - this data will be included // in the session_start event sent by the main telemetry system } catch (error) { logger.debug('Failed to log startup success:', error); } } /** * Get the list of checkpoints passed so far */ getCheckpoints(): StartupCheckpoint[] { return [...this.checkpoints]; } /** * Get startup duration in milliseconds */ getStartupDuration(): number { return Date.now() - this.startTime; } /** * Get startup data for inclusion in session_start event */ getStartupData(): { durationMs: number; checkpoints: StartupCheckpoint[] } | null { if (!this.enabled) { return null; } return { durationMs: this.getStartupDuration(), checkpoints: this.getCheckpoints(), }; } /** * Check if early logger is enabled */ isEnabled(): boolean { return this.enabled && this.supabase !== null && this.userId !== null; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/88-888/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server