@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
453 lines ⢠16.3 kB
JavaScript
/**
* 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