UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

453 lines • 16.3 kB
/** * Data Flow Tracer - Follow a single request through all system layers * * Purpose: Systematic debugging by tracing data transformations at each boundary * Usage: Attach to any request to see exactly how data changes through the pipeline * * @author Optimizely MCP Server - Chief Software Scientist Debugging Framework * @version 1.0.0 */ import { getLogger } from '../logging/Logger.js'; import { randomUUID } from 'crypto'; /** * Global trace registry - tracks active traces */ class TraceRegistry { activeSessions = new Map(); completedSessions = []; maxCompletedSessions = 100; // Keep last 100 for analysis createSession(requestType, initialData) { const traceId = `trace_${Date.now()}_${randomUUID().substring(0, 8)}`; const session = { traceId, requestType, startTime: Date.now(), points: [], summary: { totalTransformations: 0, layersTraversed: [], errorCount: 0 } }; // Add initial trace point if data provided if (initialData) { session.points.push({ layer: 'ENTRY', component: 'MCP_PROTOCOL', operation: 'REQUEST_RECEIVED', timestamp: Date.now(), input: null, output: initialData, metadata: { requestType } }); } this.activeSessions.set(traceId, session); return traceId; } addTracePoint(traceId, point) { const session = this.activeSessions.get(traceId); if (!session) return; session.points.push(point); // Update summary if (!session.summary.layersTraversed.includes(point.layer)) { session.summary.layersTraversed.push(point.layer); } if (point.transformations) { session.summary.totalTransformations += point.transformations.length; } if (point.errors && point.errors.length > 0) { session.summary.errorCount += point.errors.length; } } completeSession(traceId, finalData) { const session = this.activeSessions.get(traceId); if (!session) return null; session.endTime = Date.now(); session.summary.durationMs = session.endTime - session.startTime; // Add final trace point if data provided if (finalData) { session.points.push({ layer: 'EXIT', component: 'MCP_RESPONSE', operation: 'RESPONSE_SENT', timestamp: Date.now(), input: null, output: finalData, metadata: { totalDurationMs: session.summary.durationMs, layersTraversed: session.summary.layersTraversed.length } }); } // Move to completed sessions this.activeSessions.delete(traceId); this.completedSessions.unshift(session); // Maintain max size if (this.completedSessions.length > this.maxCompletedSessions) { this.completedSessions = this.completedSessions.slice(0, this.maxCompletedSessions); } return session; } getSession(traceId) { return this.activeSessions.get(traceId) || this.completedSessions.find(s => s.traceId === traceId) || null; } getRecentSessions(limit = 10) { return this.completedSessions.slice(0, limit); } getActiveSessionCount() { return this.activeSessions.size; } } // Global registry instance const traceRegistry = new TraceRegistry(); /** * Main DataFlowTracer class */ export class DataFlowTracer { static logger = getLogger(); /** * Start tracing a request */ static startTrace(requestType, initialData) { const traceId = traceRegistry.createSession(requestType, initialData); this.logger.info({ traceId, requestType, hasInitialData: !!initialData }, 'šŸ” TRACE STARTED'); return traceId; } /** * Add a trace point at any system boundary */ static trace(traceId, layer, component, operation, input, output, options) { if (!traceId) return; // Graceful degradation if no trace const point = { layer, component, operation, timestamp: Date.now(), input: this.sanitizeForLogging(input), output: this.sanitizeForLogging(output), ...options }; traceRegistry.addTracePoint(traceId, point); // Log the trace point this.logger.info({ traceId, layer, component, operation, inputSize: JSON.stringify(input || {}).length, outputSize: JSON.stringify(output || {}).length, transformationCount: options?.transformations?.length || 0, errorCount: options?.errors?.length || 0 }, `šŸ” TRACE: ${layer}.${component}.${operation}`); // Log transformations if any if (options?.transformations && options.transformations.length > 0) { this.logger.info({ traceId, transformations: options.transformations }, `šŸ”„ TRANSFORMATIONS in ${layer}.${component}`); } // Log errors if any if (options?.errors && options.errors.length > 0) { this.logger.warn({ traceId, errors: options.errors }, `āŒ ERRORS in ${layer}.${component}`); } } /** * Compare input vs output at a boundary */ static compareAtBoundary(traceId, layer, component, input, output) { const transformations = []; // Deep comparison to find what changed const changes = this.deepCompare(input, output); for (const change of changes) { transformations.push({ field: change.path, before: change.before, after: change.after, reason: change.type }); } return transformations; } /** * End tracing and get full session */ static endTrace(traceId, finalData) { const session = traceRegistry.completeSession(traceId, finalData); if (session) { this.logger.info({ traceId, durationMs: session.summary.durationMs, layersTraversed: session.summary.layersTraversed.length, totalTransformations: session.summary.totalTransformations, errorCount: session.summary.errorCount }, 'šŸ TRACE COMPLETED'); // Log summary of transformations if (session.summary.totalTransformations > 0) { this.generateTransformationSummary(session); } } return session; } /** * Get trace session for analysis */ static getTrace(traceId) { return traceRegistry.getSession(traceId); } /** * Get recent traces for debugging */ static getRecentTraces(limit = 10) { return traceRegistry.getRecentSessions(limit); } /** * Generate a human-readable trace report */ static generateTraceReport(traceId) { const session = traceRegistry.getSession(traceId); if (!session) return `Trace ${traceId} not found`; let report = `\nšŸ” TRACE REPORT: ${traceId}\n`; report += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; report += `Request Type: ${session.requestType}\n`; report += `Duration: ${session.summary.durationMs}ms\n`; report += `Layers: ${session.summary.layersTraversed.join(' → ')}\n`; report += `Transformations: ${session.summary.totalTransformations}\n`; report += `Errors: ${session.summary.errorCount}\n\n`; // Trace points for (let i = 0; i < session.points.length; i++) { const point = session.points[i]; const prevPoint = i > 0 ? session.points[i - 1] : null; const timeDiff = prevPoint ? point.timestamp - prevPoint.timestamp : 0; report += `${i + 1}. ${point.layer}.${point.component}.${point.operation}`; if (timeDiff > 0) report += ` (+${timeDiff}ms)`; report += `\n`; if (point.transformations && point.transformations.length > 0) { report += ` šŸ”„ Transformations:\n`; for (const t of point.transformations) { report += ` ${t.field}: ${JSON.stringify(t.before)} → ${JSON.stringify(t.after)} (${t.reason})\n`; } } if (point.errors && point.errors.length > 0) { report += ` āŒ Errors:\n`; for (const error of point.errors) { report += ` ${error}\n`; } } report += `\n`; } return report; } /** * Helper: Deep compare two objects to find changes */ static deepCompare(obj1, obj2, path = '') { const changes = []; // Handle null/undefined cases if (obj1 === obj2) return changes; if (obj1 == null || obj2 == null) { changes.push({ path: path || 'root', before: obj1, after: obj2, type: 'null_change' }); return changes; } // Handle type changes if (typeof obj1 !== typeof obj2) { changes.push({ path: path || 'root', before: obj1, after: obj2, type: 'type_change' }); return changes; } // Handle primitive values if (typeof obj1 !== 'object') { if (obj1 !== obj2) { changes.push({ path: path || 'root', before: obj1, after: obj2, type: 'value_change' }); } return changes; } // Handle arrays if (Array.isArray(obj1) || Array.isArray(obj2)) { if (!Array.isArray(obj1) || !Array.isArray(obj2)) { changes.push({ path: path || 'root', before: obj1, after: obj2, type: 'array_type_change' }); return changes; } if (obj1.length !== obj2.length) { changes.push({ path: `${path}.length`, before: obj1.length, after: obj2.length, type: 'array_length_change' }); } // Compare array elements const maxLength = Math.max(obj1.length, obj2.length); for (let i = 0; i < maxLength; i++) { const newPath = `${path}[${i}]`; changes.push(...this.deepCompare(obj1[i], obj2[i], newPath)); } return changes; } // Handle objects const keys1 = Object.keys(obj1 || {}); const keys2 = Object.keys(obj2 || {}); const allKeys = new Set([...keys1, ...keys2]); for (const key of allKeys) { const newPath = path ? `${path}.${key}` : key; if (!(key in obj1)) { changes.push({ path: newPath, before: undefined, after: obj2[key], type: 'field_added' }); } else if (!(key in obj2)) { changes.push({ path: newPath, before: obj1[key], after: undefined, type: 'field_removed' }); } else { changes.push(...this.deepCompare(obj1[key], obj2[key], newPath)); } } return changes; } /** * Helper: Sanitize data for logging (remove sensitive info, limit size) */ static sanitizeForLogging(data) { if (data == null) return data; try { const serialized = JSON.stringify(data); // Limit size for logging if (serialized.length > 10000) { return { _truncated: true, _originalSize: serialized.length, _preview: serialized.substring(0, 1000) + '...' }; } return data; } catch (error) { return { _error: 'Failed to serialize', _type: typeof data, _toString: String(data).substring(0, 100) }; } } /** * Helper: Generate transformation summary */ static generateTransformationSummary(session) { const transformationsByLayer = {}; const fieldChanges = {}; for (const point of session.points) { if (point.transformations) { transformationsByLayer[point.layer] = (transformationsByLayer[point.layer] || 0) + point.transformations.length; for (const transform of point.transformations) { fieldChanges[transform.field] = (fieldChanges[transform.field] || 0) + 1; } } } this.logger.info({ traceId: session.traceId, transformationsByLayer, mostChangedFields: Object.entries(fieldChanges) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([field, count]) => ({ field, count })) }, 'šŸ“Š TRANSFORMATION SUMMARY'); } /** * Utility: Create a traced wrapper for any function */ static traceFunction(fn, layer, component, operation) { return ((...args) => { // Extract trace ID from first argument if it exists const traceId = args[0]?.traceId || args.find((arg) => typeof arg === 'string' && arg.startsWith('trace_')); if (!traceId) { // No trace ID found, execute normally return fn(...args); } const input = args; const result = fn(...args); // Handle async functions if (result && typeof result.then === 'function') { return result.then((output) => { this.trace(traceId, layer, component, operation, input, output); return output; }).catch((error) => { this.trace(traceId, layer, component, operation, input, null, { errors: [error.message || String(error)] }); throw error; }); } else { this.trace(traceId, layer, component, operation, input, result); return result; } }); } } /** * Decorator for automatic tracing */ export function traced(layer, component, operation) { return function (target, propertyKey, descriptor) { const originalMethod = descriptor.value; const op = operation || propertyKey; descriptor.value = DataFlowTracer.traceFunction(originalMethod, layer, component, op); return descriptor; }; } /** * Helper to extract or create trace context */ export function getOrCreateTraceContext(args, requestType) { // Look for existing trace context const existingContext = args.find((arg) => arg && typeof arg === 'object' && arg.traceId); if (existingContext) { return existingContext; } // Create new trace context const traceId = DataFlowTracer.startTrace(requestType || 'unknown'); return { traceId }; } //# sourceMappingURL=DataFlowTracer.js.map