UNPKG

@guardaian/sdk

Version:

Zero-friction AI governance and monitoring SDK for Node.js applications

712 lines (625 loc) โ€ข 21.8 kB
/** * GuardAIan Client - Bulletproof Production Implementation * * CRITICAL GUARANTEES: * 1. Customer AI calls NEVER blocked, even if GuardAIan is completely down * 2. All tracking errors are isolated and non-blocking * 3. Circuit breaker prevents cascading failures * 4. Automatic recovery and retry logic * 5. Memory-safe with queue limits and cleanup */ const axios = require('axios'); class GuardAIanClient { constructor(options = {}) { this.options = { apiKey: options.apiKey, baseURL: options.baseURL || 'https://api.guardaian.ai', environment: options.environment || 'production', debug: options.debug || false, batchSize: options.batchSize || 10, flushInterval: options.flushInterval || 5000, maxRetries: options.maxRetries || 3, timeout: options.timeout || 3000, // Aggressive 3s timeout enableFallback: options.enableFallback !== false, maxQueueSize: options.maxQueueSize || 1000, ...options }; // Initialize as disabled if no API key if (!this.options.apiKey) { this.log('โš ๏ธ GuardAIan: No API key - tracking disabled (non-blocking)'); this.disabled = true; return this; } // Circuit breaker for fault tolerance this.circuitBreaker = { failures: 0, lastFailureTime: null, state: 'CLOSED', // CLOSED, OPEN, HALF_OPEN maxFailures: 5, resetTimeout: 60000 // 1 minute }; // Health tracking this.isHealthy = true; this.lastSuccessTime = Date.now(); this.totalRequests = 0; this.successfulRequests = 0; this.initializeHttpClient(); this.initializeQueue(); this.startHealthMonitoring(); this.log('๐Ÿ›ก๏ธ GuardAIan client initialized with bulletproof protection'); } /** * Initialize HTTP client with aggressive timeouts and error handling */ initializeHttpClient() { try { this.axios = axios.create({ baseURL: this.options.baseURL, headers: { 'Authorization': `Bearer ${this.options.apiKey}`, 'Content-Type': 'application/json', 'User-Agent': `@guardaian/sdk@1.0.0-beta.1`, 'X-SDK-Version': '1.0.0-beta.1' }, timeout: this.options.timeout, validateStatus: (status) => status < 500, // Don't retry 4xx errors maxRedirects: 0, // No redirects for security decompress: true }); // Request interceptor for additional safety this.axios.interceptors.request.use( (config) => { config.metadata = { startTime: Date.now() }; return config; }, (error) => { this.log(`โŒ Request interceptor error: ${error.message}`); return Promise.reject(error); } ); // Response interceptor for monitoring this.axios.interceptors.response.use( (response) => { const duration = Date.now() - response.config.metadata.startTime; this.log(`โœ… Request successful (${duration}ms)`); return response; }, (error) => { const duration = error.config?.metadata ? Date.now() - error.config.metadata.startTime : 0; this.log(`โŒ Request failed (${duration}ms): ${error.message}`); return Promise.reject(error); } ); } catch (error) { this.log(`โŒ HTTP client initialization failed: ${error.message}`); this.disabled = true; } } /** * Initialize queue with memory protection */ initializeQueue() { this.queue = []; this.isProcessing = false; this.flushTimer = null; this.startFlushTimer(); } /** * CRITICAL: Track usage with 100% error isolation * This method NEVER throws errors or blocks customer code */ async track(data) { // Immediate return conditions if (this.disabled) { this.log('โš ๏ธ Tracking disabled - returning immediately'); return null; } if (this.isCircuitBreakerOpen()) { this.log('โš ๏ธ Circuit breaker OPEN - skipping track'); return null; } try { const trackingData = this.prepareTrackingData(data); // Memory protection - prevent queue overflow if (this.queue.length >= this.options.maxQueueSize) { this.log(`โš ๏ธ Queue full (${this.queue.length}) - dropping oldest entries`); // Remove oldest 25% of entries const dropCount = Math.floor(this.options.maxQueueSize * 0.25); this.queue.splice(0, dropCount); } this.queue.push(trackingData); this.log(`๐Ÿ“Š Queued tracking data (${this.queue.length}/${this.options.maxQueueSize})`); // Trigger flush if queue is full if (this.queue.length >= this.options.batchSize) { // Fire-and-forget flush setImmediate(() => this.flush().catch(() => {})); } return trackingData; } catch (error) { // CRITICAL: Tracking errors must never affect customer code this.log(`โŒ Track error (isolated): ${error.message}`); this.recordFailure(); return null; } } /** * Flush with complete error isolation and retry logic */ async flush() { if (this.disabled || this.queue.length === 0 || this.isProcessing) { return; } if (this.isCircuitBreakerOpen()) { this.log('โš ๏ธ Circuit breaker OPEN - skipping flush'); return; } this.isProcessing = true; const batch = [...this.queue]; this.queue = []; try { this.log(`๐Ÿ“ค Flushing ${batch.length} records`); // Create request with timeout race const requestPromise = this.axios.post('/api/sdk/track-batch', { usage: batch, metadata: { sdkVersion: '1.0.0-beta.1', batchSize: batch.length, timestamp: new Date().toISOString() } }); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Flush timeout')), this.options.timeout); }); const response = await Promise.race([requestPromise, timeoutPromise]); // Success handling this.recordSuccess(); this.log(`โœ… Flushed ${batch.length} records successfully`); if (this.options.debug && response.data) { console.log('๐Ÿ›ก๏ธ GuardAIan response:', response.data); } } catch (error) { this.handleFlushError(error, batch); } finally { this.isProcessing = false; } } /** * Handle flush errors with intelligent retry and circuit breaker */ handleFlushError(error, batch) { this.recordFailure(); const errorType = this.categorizeError(error); this.log(`โŒ Flush failed (${errorType}): ${error.message}`); // Intelligent retry logic based on error type if (this.shouldRetry(errorType, error)) { this.log(`๐Ÿ”„ Queuing batch for retry (${this.circuitBreaker.failures} failures)`); // Add failed batch back to front of queue (priority) const retryBatch = batch.slice(0, Math.min(100, batch.length)); // Limit retry size this.queue.unshift(...retryBatch); } else { this.log(`๐Ÿšซ Dropping batch - not retryable (${errorType})`); } // Emergency memory protection if (this.queue.length > this.options.maxQueueSize) { const excessCount = this.queue.length - this.options.maxQueueSize; this.queue.splice(0, excessCount); this.log(`โš ๏ธ Emergency queue truncation - dropped ${excessCount} entries`); } } /** * Determine if error should be retried */ shouldRetry(errorType, error) { // Don't retry client errors (4xx) or auth errors if (errorType === 'client' || error.response?.status === 401 || error.response?.status === 403) { return false; } // Don't retry if circuit breaker limits exceeded if (this.circuitBreaker.failures >= this.options.maxRetries) { return false; } // Retry network and server errors return errorType === 'network' || errorType === 'server' || errorType === 'timeout'; } /** * Circuit breaker implementation */ isCircuitBreakerOpen() { const now = Date.now(); switch (this.circuitBreaker.state) { case 'OPEN': // Check if we should try half-open if (now - this.circuitBreaker.lastFailureTime > this.circuitBreaker.resetTimeout) { this.circuitBreaker.state = 'HALF_OPEN'; this.log('๐Ÿ”„ Circuit breaker: OPEN -> HALF_OPEN (testing)'); return false; } return true; case 'HALF_OPEN': // Allow one request to test if service is back return false; case 'CLOSED': default: return false; } } /** * Record successful operation */ recordSuccess() { this.totalRequests++; this.successfulRequests++; this.circuitBreaker.failures = 0; this.circuitBreaker.lastFailureTime = null; this.circuitBreaker.state = 'CLOSED'; this.isHealthy = true; this.lastSuccessTime = Date.now(); } /** * Record failed operation */ recordFailure() { this.totalRequests++; this.circuitBreaker.failures++; this.circuitBreaker.lastFailureTime = Date.now(); this.isHealthy = false; // Open circuit breaker if failure threshold exceeded if (this.circuitBreaker.failures >= this.circuitBreaker.maxFailures) { this.circuitBreaker.state = 'OPEN'; this.log(`๐Ÿšซ Circuit breaker: CLOSED -> OPEN (${this.circuitBreaker.failures} failures)`); } } /** * Categorize errors for appropriate handling */ categorizeError(error) { // Network connectivity errors if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT' || error.code === 'ECONNRESET' || error.message.includes('timeout')) { return 'network'; } // Client errors (4xx) if (error.response?.status >= 400 && error.response?.status < 500) { return 'client'; } // Server errors (5xx) if (error.response?.status >= 500) { return 'server'; } // Request timeout if (error.message.includes('timeout') || error.code === 'ECONNABORTED') { return 'timeout'; } return 'unknown'; } /** * Prepare tracking data with comprehensive error protection */ prepareTrackingData(data) { try { return { service: this.sanitizeString(data.service) || 'unknown', model: this.sanitizeString(data.model) || 'unknown', operation: this.sanitizeString(data.operation) || 'unknown', inputTokens: this.sanitizeNumber(data.inputTokens, 0), outputTokens: this.sanitizeNumber(data.outputTokens, 0), totalTokens: this.sanitizeNumber(data.totalTokens, (data.inputTokens || 0) + (data.outputTokens || 0)), cost: this.sanitizeNumber(data.cost, 0), duration: this.sanitizeNumber(data.duration, 0), requestData: this.sanitizeRequestData(data.requestData), responseData: this.sanitizeResponseData(data.responseData), metadata: { environment: this.sanitizeString(this.options.environment), projectName: this.sanitizeString(data.projectName || process.env.npm_package_name) || 'unknown', userAgent: '@guardaian/sdk@1.0.0-beta.1', customTags: Array.isArray(data.tags) ? data.tags.slice(0, 10) : [], // Limit tags sdkVersion: '1.0.0-beta.1', nodeVersion: process.version, ...this.sanitizeMetadata(data.metadata) }, timestamp: new Date().toISOString() }; } catch (error) { this.log(`โŒ Data preparation error: ${error.message}`); return { service: 'unknown', model: 'unknown', operation: 'error', inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0, duration: 0, error: error.message, timestamp: new Date().toISOString() }; } } /** * Sanitize string values */ sanitizeString(value) { if (typeof value !== 'string') return null; return value.substring(0, 200); // Limit length } /** * Sanitize numeric values */ sanitizeNumber(value, defaultValue = 0) { const num = Number(value); return isNaN(num) || !isFinite(num) || num < 0 ? defaultValue : Math.round(num * 10000) / 10000; } /** * Sanitize metadata object */ sanitizeMetadata(metadata) { if (!metadata || typeof metadata !== 'object') return {}; try { const sanitized = {}; Object.keys(metadata).slice(0, 20).forEach(key => { // Limit to 20 keys const value = metadata[key]; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { sanitized[key] = typeof value === 'string' ? value.substring(0, 200) : value; } }); return sanitized; } catch (error) { this.log(`โŒ Metadata sanitization error: ${error.message}`); return {}; } } /** * Sanitize request data with privacy protection */ sanitizeRequestData(data) { try { if (!data) return {}; const sanitized = { ...data }; // Handle messages array (common in AI APIs) if (sanitized.messages && Array.isArray(sanitized.messages)) { sanitized.messages = sanitized.messages.slice(0, 10).map(msg => ({ role: msg.role, content: typeof msg.content === 'string' ? msg.content.substring(0, 200) + (msg.content.length > 200 ? '...' : '') : '[complex_content]' })); } // Remove sensitive fields delete sanitized.apiKey; delete sanitized.authorization; delete sanitized.api_key; delete sanitized.bearer; delete sanitized.token; // Limit object size const keys = Object.keys(sanitized); if (keys.length > 20) { const limitedSanitized = {}; keys.slice(0, 20).forEach(key => { limitedSanitized[key] = sanitized[key]; }); return limitedSanitized; } return sanitized; } catch (error) { this.log(`โŒ Request sanitization error: ${error.message}`); return { error: 'sanitization_failed' }; } } /** * Sanitize response data with privacy protection */ sanitizeResponseData(data) { try { if (!data) return {}; return { id: data.id, object: data.object, created: data.created, model: data.model, usage: data.usage, choices: data.choices ? data.choices.length : 0, finish_reason: data.finish_reason, // Don't include actual response content for privacy response_length: typeof data.text === 'string' ? data.text.length : 0 }; } catch (error) { this.log(`โŒ Response sanitization error: ${error.message}`); return { error: 'sanitization_failed' }; } } /** * Start health monitoring and recovery systems */ startHealthMonitoring() { // Health check every minute setInterval(() => { try { this.performHealthCheck(); } catch (error) { this.log(`โŒ Health check error: ${error.message}`); } }, 60000); // Memory monitoring every 5 minutes setInterval(() => { try { this.monitorMemoryUsage(); } catch (error) { this.log(`โŒ Memory monitoring error: ${error.message}`); } }, 300000); } /** * Perform health check and auto-recovery */ performHealthCheck() { const now = Date.now(); const timeSinceLastSuccess = now - this.lastSuccessTime; const successRate = this.totalRequests > 0 ? this.successfulRequests / this.totalRequests : 1; // Auto-recovery logic if (timeSinceLastSuccess > 300000) { // 5 minutes without success this.log('๐Ÿ”„ Health check: Long time without success, attempting recovery'); // Gradually reduce failure count this.circuitBreaker.failures = Math.max(0, this.circuitBreaker.failures - 1); // Try to move from OPEN to HALF_OPEN if (this.circuitBreaker.state === 'OPEN') { this.circuitBreaker.state = 'HALF_OPEN'; this.log('๐Ÿ”„ Circuit breaker: Auto recovery OPEN -> HALF_OPEN'); } } // Log health metrics if (this.options.debug) { this.log(`๐Ÿ“Š Health: ${(successRate * 100).toFixed(1)}% success rate, queue: ${this.queue.length}, circuit: ${this.circuitBreaker.state}`); } } /** * Monitor memory usage and cleanup if needed */ monitorMemoryUsage() { const memUsage = process.memoryUsage(); const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024); // Emergency cleanup if memory usage is high if (heapUsedMB > 500) { // 500MB threshold this.log(`โš ๏ธ High memory usage (${heapUsedMB}MB) - performing emergency cleanup`); // Clear queue if too large if (this.queue.length > 100) { this.queue = this.queue.slice(-50); // Keep only latest 50 this.log('๐Ÿงน Emergency queue cleanup completed'); } } } /** * Start flush timer with error protection */ startFlushTimer() { if (this.flushTimer) { clearInterval(this.flushTimer); } this.flushTimer = setInterval(() => { try { // Fire-and-forget flush setImmediate(() => this.flush().catch(() => {})); } catch (error) { this.log(`โŒ Timer flush error: ${error.message}`); } }, this.options.flushInterval); } /** * Calculate cost with error protection and accurate pricing */ calculateCost(service, model, usage) { try { const costMap = { openai: { 'gpt-4': { input: 0.03 / 1000, output: 0.06 / 1000 }, 'gpt-4-turbo': { input: 0.01 / 1000, output: 0.03 / 1000 }, 'gpt-4o': { input: 0.005 / 1000, output: 0.015 / 1000 }, 'gpt-3.5-turbo': { input: 0.0015 / 1000, output: 0.002 / 1000 }, 'text-embedding-ada-002': { input: 0.0001 / 1000, output: 0 }, 'text-embedding-3-small': { input: 0.00002 / 1000, output: 0 }, 'text-embedding-3-large': { input: 0.00013 / 1000, output: 0 } }, anthropic: { 'claude-3-5-sonnet-20241022': { input: 0.003 / 1000, output: 0.015 / 1000 }, 'claude-3-5-haiku-20241022': { input: 0.001 / 1000, output: 0.005 / 1000 }, 'claude-3-opus-20240229': { input: 0.015 / 1000, output: 0.075 / 1000 }, 'claude-3-sonnet-20240229': { input: 0.003 / 1000, output: 0.015 / 1000 }, 'claude-3-haiku-20240307': { input: 0.00025 / 1000, output: 0.00125 / 1000 } }, google: { 'gemini-pro': { input: 0, output: 0 }, // Free tier 'gemini-1.5-pro': { input: 0.0035 / 1000, output: 0.0105 / 1000 }, 'gemini-1.5-flash': { input: 0.00035 / 1000, output: 0.00105 / 1000 } } }; const pricing = costMap[service]?.[model]; if (!pricing) { this.log(`โš ๏ธ Unknown pricing for ${service}/${model}`); return 0; } const inputCost = (usage.inputTokens || usage.prompt_tokens || 0) * pricing.input; const outputCost = (usage.outputTokens || usage.completion_tokens || 0) * pricing.output; return Math.round((inputCost + outputCost) * 10000) / 10000; // Round to 4 decimal places } catch (error) { this.log(`โŒ Cost calculation error: ${error.message}`); return 0; } } /** * Get comprehensive health status */ getHealthStatus() { try { const successRate = this.totalRequests > 0 ? this.successfulRequests / this.totalRequests : 1; return { isHealthy: this.isHealthy, disabled: this.disabled, circuitBreaker: { state: this.circuitBreaker.state, failures: this.circuitBreaker.failures, lastFailureTime: this.circuitBreaker.lastFailureTime }, queue: { size: this.queue.length, maxSize: this.options.maxQueueSize, isProcessing: this.isProcessing }, metrics: { totalRequests: this.totalRequests, successfulRequests: this.successfulRequests, successRate: Math.round(successRate * 10000) / 100, // Percentage with 2 decimals lastSuccessTime: this.lastSuccessTime }, timestamp: new Date().toISOString() }; } catch (error) { return { error: error.message, timestamp: new Date().toISOString() }; } } /** * Debug logging with error protection */ log(message) { try { if (this.options.debug) { console.log(`๐Ÿ›ก๏ธ GuardAIan: ${message}`); } } catch (error) { // Even logging can fail - do nothing to prevent infinite loops } } /** * Graceful cleanup with error protection */ destroy() { try { // Clear timer if (this.flushTimer) { clearInterval(this.flushTimer); this.flushTimer = null; } // Final flush attempt (fire-and-forget) if (this.queue.length > 0 && !this.disabled) { setImmediate(() => { this.flush().catch(() => { this.log('โŒ Final flush failed during cleanup'); }); }); } // Clear references this.queue = []; this.axios = null; this.log('๐Ÿ›ก๏ธ GuardAIan client destroyed gracefully'); } catch (error) { // Cleanup should never fail catastrophically console.warn('โš ๏ธ GuardAIan cleanup error (non-blocking):', error.message); } } } module.exports = GuardAIanClient;