@guardaian/sdk
Version:
Zero-friction AI governance and monitoring SDK for Node.js applications
712 lines (625 loc) โข 21.8 kB
JavaScript
/**
* 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': `/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;