UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

677 lines 35.3 kB
import { BaseToolHandler } from '../base-handler.js'; import { LocalDebugEngine } from '../../local-debug-engine.js'; import { GraphQLExtractionUtils } from './graphql-utils.js'; export class GraphQLHandler extends BaseToolHandler { tools = [ { name: 'trace_graphql_queries', description: 'Monitor all GraphQL requests/responses with performance metrics and error tracking', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, operationType: { type: 'string', description: 'Filter by operation type (query, mutation, subscription)', enum: ['query', 'mutation', 'subscription', 'all'], default: 'all' }, includeVariables: { type: 'boolean', description: 'Include query variables in output', default: false }, minDuration: { type: 'number', description: 'Only show operations taking longer than X ms', default: 0 } }, required: ['sessionId'] } }, { name: 'analyze_query_performance', description: 'Detect N+1 queries, expensive operations, and performance bottlenecks in GraphQL', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, complexityThreshold: { type: 'number', description: 'Complexity threshold for warnings', default: 50 }, detectNPlusOne: { type: 'boolean', description: 'Detect N+1 query patterns', default: true } }, required: ['sessionId'] } }, { name: 'validate_schema_usage', description: 'Analyze field usage patterns, find unused fields, and detect deprecated usage', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, showUnused: { type: 'boolean', description: 'Show potentially unused fields', default: true }, showDeprecated: { type: 'boolean', description: 'Show deprecated field usage', default: true } }, required: ['sessionId'] } }, { name: 'debug_resolver_errors', description: 'Track resolver failures, error patterns, and debugging information', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, groupByType: { type: 'boolean', description: 'Group errors by type', default: true }, includeStackTrace: { type: 'boolean', description: 'Include stack traces when available', default: false } }, required: ['sessionId'] } }, { name: 'monitor_query_complexity', description: 'Analyze query depth, complexity scores, and security implications', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, maxDepth: { type: 'number', description: 'Maximum safe query depth', default: 10 }, maxComplexity: { type: 'number', description: 'Maximum safe complexity score', default: 100 } }, required: ['sessionId'] } }, { name: 'inspect_graphql_cache', description: 'Analyze cache hit rates, cache invalidation patterns, and caching opportunities', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, showMisses: { type: 'boolean', description: 'Show cache misses and opportunities', default: true } }, required: ['sessionId'] } } ]; async handle(toolName, args, sessions) { const session = sessions.get(args.sessionId); if (!session) { return { content: [{ type: 'text', text: `Session not found: ${args.sessionId}` }] }; } try { switch (toolName) { case 'trace_graphql_queries': return await this.traceGraphQLQueries(args, session); case 'analyze_query_performance': return await this.analyzeQueryPerformance(args, session); case 'validate_schema_usage': return await this.validateSchemaUsage(args, session); case 'debug_resolver_errors': return await this.debugResolverErrors(args, session); case 'monitor_query_complexity': return await this.monitorQueryComplexity(args, session); case 'inspect_graphql_cache': return await this.inspectGraphQLCache(args, session); default: throw new Error(`Unknown tool: ${toolName}`); } } catch (error) { return this.createErrorResponse(`Error in ${toolName}: ${error instanceof Error ? error.message : String(error)}`); } } async traceGraphQLQueries(args, session) { const engine = session.engine || new LocalDebugEngine(); const networkRequests = engine.getNetworkRequests(); const operations = GraphQLExtractionUtils.extractGraphQLOperations(networkRequests); // Apply filters const filteredOps = operations.filter(op => { if (args.operationType && args.operationType !== 'all' && op.type !== args.operationType) { return false; } if (args.minDuration && (!op.duration || op.duration < args.minDuration)) { return false; } return true; }); if (filteredOps.length === 0) { return this.createTextResponse('## GraphQL Query Trace\n\nNo GraphQL operations detected. Make sure you have GraphQL traffic in your application.'); } let report = '## GraphQL Query Trace\n\n'; // Summary statistics const totalOps = filteredOps.length; const avgDuration = filteredOps.reduce((sum, op) => sum + (op.duration || 0), 0) / totalOps; const errorCount = filteredOps.filter(op => op.errors && op.errors.length > 0).length; const typeDistribution = this.getOperationTypeDistribution(filteredOps); report += '### Summary\n'; report += `- **Total Operations:** ${totalOps}\n`; report += `- **Average Duration:** ${avgDuration.toFixed(2)}ms\n`; report += `- **Operations with Errors:** ${errorCount}\n`; report += `- **Type Distribution:** ${Object.entries(typeDistribution).map(([type, count]) => `${type}: ${count}`).join(', ')}\n\n`; // Individual operations report += '### Operation Details\n\n'; for (const [index, operation] of filteredOps.entries()) { report += `#### Operation ${index + 1}\n`; report += `- **Type:** ${operation.type}\n`; if (operation.name) { report += `- **Name:** ${operation.name}\n`; } report += `- **Timestamp:** ${operation.timestamp.toISOString()}\n`; if (operation.duration) { report += `- **Duration:** ${operation.duration}ms\n`; } if (operation.client?.name) { report += `- **Client:** ${operation.client.name}${operation.client.version ? ` v${operation.client.version}` : ''}\n`; } if (operation.performance) { report += `- **Complexity:** ${operation.performance.complexity || 'Unknown'}\n`; report += `- **Depth:** ${operation.performance.depth || 'Unknown'}\n`; report += `- **Field Count:** ${operation.performance.fieldCount || 'Unknown'}\n`; } if (operation.errors && operation.errors.length > 0) { report += `- **Errors:** ${operation.errors.length}\n`; for (const error of operation.errors) { report += ` - ${error.message}\n`; } } if (args.includeVariables && operation.variables) { report += `- **Variables:** \`\`\`json\n${JSON.stringify(operation.variables, null, 2)}\n\`\`\`\n`; } report += `- **Query:**\n\`\`\`graphql\n${operation.query}\n\`\`\`\n\n`; } return this.createTextResponse(report); } async analyzeQueryPerformance(args, session) { const engine = session.engine || new LocalDebugEngine(); const networkRequests = engine.getNetworkRequests(); const metrics = GraphQLExtractionUtils.analyzeQueryPerformance(networkRequests); const nPlusOnePatterns = args.detectNPlusOne ? GraphQLExtractionUtils.detectNPlusOneQueries(networkRequests) : []; if (metrics.length === 0) { return this.createTextResponse('## GraphQL Performance Analysis\n\nNo GraphQL operations with performance data found.'); } let report = '## GraphQL Performance Analysis\n\n'; // Performance overview const avgDuration = metrics.reduce((sum, m) => sum + m.duration, 0) / metrics.length; const slowQueries = metrics.filter(m => m.duration > 1000); // > 1 second const cacheHits = metrics.filter(m => m.cacheHit).length; const cacheHitRate = (cacheHits / metrics.length) * 100; report += '### Performance Overview\n'; report += `- **Total Operations:** ${metrics.length}\n`; report += `- **Average Duration:** ${avgDuration.toFixed(2)}ms\n`; report += `- **Slow Queries (>1s):** ${slowQueries.length}\n`; report += `- **Cache Hit Rate:** ${cacheHitRate.toFixed(1)}%\n\n`; // N+1 query detection if (nPlusOnePatterns.length > 0) { report += '### ⚠️ N+1 Query Patterns Detected\n\n'; for (const pattern of nPlusOnePatterns) { report += `#### Pattern: ${pattern.occurrences} similar operations\n`; report += `- **Occurrences:** ${pattern.occurrences}\n`; report += `- **Suggestion:** ${pattern.suggestion}\n`; report += `- **Sample Query:**\n\`\`\`graphql\n${pattern.operations[0].query}\n\`\`\`\n\n`; } } // Slow query analysis if (slowQueries.length > 0) { report += '### 🐌 Slow Queries Analysis\n\n'; const sortedSlowQueries = slowQueries.sort((a, b) => b.duration - a.duration); for (const query of sortedSlowQueries.slice(0, 5)) { // Top 5 slowest report += `#### ${query.operation} - ${query.duration}ms\n`; report += `- **Duration:** ${query.duration}ms\n`; report += `- **Timestamp:** ${query.timestamp.toISOString()}\n`; if (query.complexity) { report += `- **Complexity:** ${query.complexity}\n`; } if (query.resolverStats) { const slowestResolver = Object.entries(query.resolverStats) .sort(([, a], [, b]) => b.avgTime - a.avgTime)[0]; if (slowestResolver) { report += `- **Slowest Resolver:** ${slowestResolver[0]} (${slowestResolver[1].avgTime.toFixed(2)}ms avg)\n`; } } report += '\n'; } } // Complexity warnings const operations = GraphQLExtractionUtils.extractGraphQLOperations(networkRequests); const complexQueries = operations.filter(op => op.performance?.complexity && op.performance.complexity > args.complexityThreshold); if (complexQueries.length > 0) { report += '### ⚡ High Complexity Queries\n\n'; for (const query of complexQueries) { report += `#### ${query.name || query.type} - Complexity: ${query.performance?.complexity}\n`; report += `- **Depth:** ${query.performance?.depth}\n`; report += `- **Field Count:** ${query.performance?.fieldCount}\n`; if (query.duration) { report += `- **Duration:** ${query.duration}ms\n`; } report += '\n'; } } // Recommendations report += '### 🎯 Performance Recommendations\n\n'; if (nPlusOnePatterns.length > 0) { report += '1. **Fix N+1 Queries**: Use DataLoader or include required fields in parent queries\n'; } if (cacheHitRate < 50) { report += '2. **Improve Caching**: Consider implementing query-level caching\n'; } if (slowQueries.length > metrics.length * 0.1) { report += '3. **Optimize Slow Queries**: Add database indexes or optimize resolvers\n'; } if (complexQueries.length > 0) { report += '4. **Reduce Query Complexity**: Consider query complexity limits or field selection optimization\n'; } return { content: [{ type: 'text', text: report }] }; } async validateSchemaUsage(args, session) { const engine = session.engine || new LocalDebugEngine(); const networkRequests = engine.getNetworkRequests(); const schemaUsage = GraphQLExtractionUtils.analyzeSchemaUsage(networkRequests); if (schemaUsage.length === 0) { return { content: [{ type: 'text', text: '## GraphQL Schema Usage Analysis\n\nNo GraphQL field usage data found.' }] }; } let report = '## GraphQL Schema Usage Analysis\n\n'; // Usage overview const totalFields = schemaUsage.length; const deprecatedFields = schemaUsage.filter(f => f.deprecated); const recentlyUsed = schemaUsage.filter(f => Date.now() - f.lastUsed.getTime() < 7 * 24 * 60 * 60 * 1000 // Last 7 days ); report += '### Usage Overview\n'; report += `- **Total Fields Analyzed:** ${totalFields}\n`; report += `- **Recently Used (7 days):** ${recentlyUsed.length}\n`; report += `- **Deprecated Fields:** ${deprecatedFields.length}\n\n`; // Most used fields const sortedByUsage = [...schemaUsage].sort((a, b) => b.usageCount - a.usageCount); report += '### Most Used Fields\n\n'; for (const field of sortedByUsage.slice(0, 10)) { report += `- **${field.type}.${field.field}**: ${field.usageCount} uses (last: ${field.lastUsed.toLocaleDateString()})\n`; } report += '\n'; // Deprecated field usage if (args.showDeprecated && deprecatedFields.length > 0) { report += '### ⚠️ Deprecated Field Usage\n\n'; for (const field of deprecatedFields) { report += `- **${field.type}.${field.field}**: ${field.usageCount} uses (last: ${field.lastUsed.toLocaleDateString()})\n`; } report += '\n'; } // Potentially unused fields if (args.showUnused) { const oldUsage = schemaUsage.filter(f => Date.now() - f.lastUsed.getTime() > 30 * 24 * 60 * 60 * 1000 // > 30 days ); if (oldUsage.length > 0) { report += '### 🗑️ Potentially Unused Fields (>30 days)\n\n'; for (const field of oldUsage.slice(0, 10)) { report += `- **${field.type}.${field.field}**: Last used ${field.lastUsed.toLocaleDateString()}\n`; } report += '\n'; } } // Type distribution const typeDistribution = new Map(); for (const field of schemaUsage) { typeDistribution.set(field.type, (typeDistribution.get(field.type) || 0) + 1); } report += '### Type Distribution\n\n'; for (const [type, count] of [...typeDistribution.entries()].sort((a, b) => b[1] - a[1])) { report += `- **${type}**: ${count} fields\n`; } return { content: [{ type: 'text', text: report }] }; } async debugResolverErrors(args, session) { const engine = session.engine || new LocalDebugEngine(); const networkRequests = engine.getNetworkRequests(); const errors = GraphQLExtractionUtils.extractGraphQLErrors(networkRequests); if (errors.length === 0) { return { content: [{ type: 'text', text: '## GraphQL Resolver Errors\n\nNo GraphQL errors detected. Great job! 🎉' }] }; } let report = '## GraphQL Resolver Errors\n\n'; // Error summary report += '### Error Summary\n'; report += `- **Total Errors:** ${errors.length}\n`; report += `- **Operations with Errors:** ${new Set(errors.map(e => e.operation)).size}\n`; report += `- **Time Range:** ${new Date(Math.min(...errors.map(e => e.timestamp.getTime()))).toISOString()} to ${new Date(Math.max(...errors.map(e => e.timestamp.getTime()))).toISOString()}\n\n`; // Error grouping if (args.groupByType) { const errorGroups = new Map(); for (const error of errors) { const errorType = error.extensions?.code || error.message.split(':')[0] || 'UNKNOWN'; if (!errorGroups.has(errorType)) { errorGroups.set(errorType, []); } errorGroups.get(errorType).push(error); } report += '### Errors by Type\n\n'; for (const [type, groupErrors] of [...errorGroups.entries()].sort((a, b) => b[1].length - a[1].length)) { report += `#### ${type} (${groupErrors.length} occurrences)\n`; // Show sample error const sampleError = groupErrors[0]; report += `- **Sample Message:** ${sampleError.message}\n`; if (sampleError.path) { report += `- **Path:** ${sampleError.path.join('.')}\n`; } if (sampleError.locations) { report += `- **Location:** Line ${sampleError.locations[0].line}, Column ${sampleError.locations[0].column}\n`; } report += `- **First Occurrence:** ${sampleError.timestamp.toISOString()}\n`; if (args.includeStackTrace && sampleError.extensions?.stacktrace) { report += `- **Stack Trace:**\n\`\`\`\n${sampleError.extensions.stacktrace}\n\`\`\`\n`; } report += '\n'; } } else { // List all errors chronologically report += '### Error Timeline\n\n'; const sortedErrors = [...errors].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); for (const error of sortedErrors.slice(0, 20)) { // Latest 20 errors report += `#### ${error.timestamp.toISOString()}\n`; report += `- **Message:** ${error.message}\n`; if (error.operation) { report += `- **Operation:** ${error.operation}\n`; } if (error.path) { report += `- **Path:** ${error.path.join('.')}\n`; } if (error.extensions?.code) { report += `- **Code:** ${error.extensions.code}\n`; } report += '\n'; } } // Error frequency analysis const recentErrors = errors.filter(e => Date.now() - e.timestamp.getTime() < 60 * 60 * 1000 // Last hour ); if (recentErrors.length > 0) { report += '### 🚨 Recent Error Activity (Last Hour)\n'; report += `- **Errors in Last Hour:** ${recentErrors.length}\n`; report += `- **Error Rate:** ${(recentErrors.length / 60).toFixed(2)} errors/minute\n\n`; } // Recommendations report += '### 🎯 Error Resolution Recommendations\n\n'; const errorTypes = new Set(errors.map(e => e.extensions?.code || 'UNKNOWN')); if (errorTypes.has('GRAPHQL_VALIDATION_FAILED')) { report += '1. **Validation Errors**: Check query syntax and schema compatibility\n'; } if (errorTypes.has('INTERNAL_SERVER_ERROR')) { report += '2. **Server Errors**: Review resolver implementations and error handling\n'; } if (errorTypes.has('UNAUTHENTICATED')) { report += '3. **Authentication**: Verify authentication tokens and permissions\n'; } if (recentErrors.length > 10) { report += '4. **High Error Rate**: Consider implementing circuit breakers or rate limiting\n'; } return { content: [{ type: 'text', text: report }] }; } async monitorQueryComplexity(args, session) { const engine = session.engine || new LocalDebugEngine(); const networkRequests = engine.getNetworkRequests(); const operations = GraphQLExtractionUtils.extractGraphQLOperations(networkRequests); const complexityData = operations.map(op => ({ operation: op.name || `${op.type}_${op.timestamp.getTime()}`, complexity: GraphQLExtractionUtils.calculateQueryComplexity(op.query), timestamp: op.timestamp, duration: op.duration })); if (complexityData.length === 0) { return { content: [{ type: 'text', text: '## GraphQL Query Complexity Monitor\n\nNo GraphQL operations found for complexity analysis.' }] }; } let report = '## GraphQL Query Complexity Monitor\n\n'; // Complexity overview const avgComplexity = complexityData.reduce((sum, d) => sum + d.complexity.complexity, 0) / complexityData.length; const maxComplexity = Math.max(...complexityData.map(d => d.complexity.complexity)); const dangerousQueries = complexityData.filter(d => d.complexity.depth > args.maxDepth || d.complexity.complexity > args.maxComplexity); report += '### Complexity Overview\n'; report += `- **Total Queries:** ${complexityData.length}\n`; report += `- **Average Complexity:** ${avgComplexity.toFixed(2)}\n`; report += `- **Maximum Complexity:** ${maxComplexity}\n`; report += `- **Dangerous Queries:** ${dangerousQueries.length}\n\n`; // Security analysis if (dangerousQueries.length > 0) { report += '### 🚨 Security Concerns\n\n'; const deepQueries = dangerousQueries.filter(d => d.complexity.depth > args.maxDepth); const complexQueries = dangerousQueries.filter(d => d.complexity.complexity > args.maxComplexity); if (deepQueries.length > 0) { report += `#### Deep Queries (>${args.maxDepth} levels)\n`; for (const query of deepQueries.slice(0, 5)) { report += `- **${query.operation}**: Depth ${query.complexity.depth}\n`; } report += '\n'; } if (complexQueries.length > 0) { report += `#### Complex Queries (>${args.maxComplexity} complexity)\n`; for (const query of complexQueries.slice(0, 5)) { report += `- **${query.operation}**: Complexity ${query.complexity.complexity}\n`; } report += '\n'; } } // Complexity distribution report += '### Complexity Distribution\n\n'; const complexityRanges = [ { min: 0, max: 10, label: 'Simple (0-10)' }, { min: 11, max: 50, label: 'Moderate (11-50)' }, { min: 51, max: 100, label: 'Complex (51-100)' }, { min: 101, max: Infinity, label: 'Very Complex (>100)' } ]; for (const range of complexityRanges) { const count = complexityData.filter(d => d.complexity.complexity >= range.min && d.complexity.complexity <= range.max).length; const percentage = (count / complexityData.length * 100).toFixed(1); report += `- **${range.label}**: ${count} queries (${percentage}%)\n`; } report += '\n'; // Warnings and recommendations const allWarnings = complexityData.flatMap(d => d.complexity.warnings); if (allWarnings.length > 0) { report += '### ⚠️ Complexity Warnings\n\n'; const warningCounts = new Map(); for (const warning of allWarnings) { warningCounts.set(warning, (warningCounts.get(warning) || 0) + 1); } for (const [warning, count] of [...warningCounts.entries()].sort((a, b) => b[1] - a[1])) { report += `- **${warning}** (${count} occurrences)\n`; } report += '\n'; } // Performance correlation const queriesWithDuration = complexityData.filter(d => d.duration !== undefined); if (queriesWithDuration.length > 0) { report += '### Performance Correlation\n\n'; // Find correlation between complexity and duration const highComplexitySlow = queriesWithDuration.filter(d => d.complexity.complexity > avgComplexity && d.duration > 1000); if (highComplexitySlow.length > 0) { report += `- **High complexity + slow queries**: ${highComplexitySlow.length} found\n`; report += '- Consider optimizing complex queries for better performance\n'; } const avgDurationByComplexity = { simple: queriesWithDuration.filter(d => d.complexity.complexity <= 10).reduce((sum, d) => sum + d.duration, 0) / queriesWithDuration.filter(d => d.complexity.complexity <= 10).length || 0, complex: queriesWithDuration.filter(d => d.complexity.complexity > 50).reduce((sum, d) => sum + d.duration, 0) / queriesWithDuration.filter(d => d.complexity.complexity > 50).length || 0 }; if (avgDurationByComplexity.complex > avgDurationByComplexity.simple * 2) { report += `- **Performance impact**: Complex queries are ${(avgDurationByComplexity.complex / avgDurationByComplexity.simple).toFixed(1)}x slower on average\n`; } } // Security recommendations report += '\n### 🛡️ Security Recommendations\n\n'; if (dangerousQueries.length > 0) { report += '1. **Implement Query Complexity Limits**: Consider rejecting queries above safe thresholds\n'; } if (maxComplexity > 200) { report += '2. **Query Depth Limiting**: Implement maximum query depth restrictions\n'; } if (complexityData.some(d => d.complexity.fieldCount > 100)) { report += '3. **Field Selection Limits**: Consider limiting the number of fields per query\n'; } report += '4. **Rate Limiting**: Implement query-based rate limiting for expensive operations\n'; return { content: [{ type: 'text', text: report }] }; } async inspectGraphQLCache(args, session) { const engine = session.engine || new LocalDebugEngine(); const networkRequests = engine.getNetworkRequests(); const metrics = GraphQLExtractionUtils.analyzeQueryPerformance(networkRequests); const operations = GraphQLExtractionUtils.extractGraphQLOperations(networkRequests); if (metrics.length === 0) { return { content: [{ type: 'text', text: '## GraphQL Cache Analysis\n\nNo GraphQL operations found for cache analysis.' }] }; } let report = '## GraphQL Cache Analysis\n\n'; // Cache overview const totalQueries = metrics.length; const cacheHits = metrics.filter(m => m.cacheHit).length; const cacheMisses = totalQueries - cacheHits; const hitRate = (cacheHits / totalQueries * 100).toFixed(1); report += '### Cache Performance\n'; report += `- **Total Queries:** ${totalQueries}\n`; report += `- **Cache Hits:** ${cacheHits}\n`; report += `- **Cache Misses:** ${cacheMisses}\n`; report += `- **Hit Rate:** ${hitRate}%\n\n`; // Cache effectiveness const effectiveness = this.evaluateCacheEffectiveness(parseFloat(hitRate)); report += `### Cache Effectiveness: ${effectiveness.rating}\n`; report += `${effectiveness.description}\n\n`; // Query patterns analysis const queryPatterns = new Map(); for (let i = 0; i < operations.length; i++) { const operation = operations[i]; const metric = metrics[i]; const signature = this.generateCacheKey(operation.query); if (!queryPatterns.has(signature)) { queryPatterns.set(signature, { hits: 0, misses: 0, operations: [] }); } const pattern = queryPatterns.get(signature); pattern.operations.push(operation); if (metric?.cacheHit) { pattern.hits++; } else { pattern.misses++; } } // Most cacheable patterns const cacheablePatterns = [...queryPatterns.entries()] .filter(([, pattern]) => pattern.operations.length > 1) .map(([signature, pattern]) => ({ signature, total: pattern.hits + pattern.misses, hitRate: (pattern.hits / (pattern.hits + pattern.misses) * 100).toFixed(1), operations: pattern.operations })) .sort((a, b) => b.total - a.total); if (cacheablePatterns.length > 0) { report += '### Most Repeated Queries\n\n'; for (const pattern of cacheablePatterns.slice(0, 5)) { report += `#### Query repeated ${pattern.total} times (${pattern.hitRate}% hit rate)\n`; report += `\`\`\`graphql\n${pattern.operations[0].query.substring(0, 200)}${pattern.operations[0].query.length > 200 ? '...' : ''}\n\`\`\`\n\n`; } } // Cache misses analysis if (args.showMisses && cacheMisses > 0) { const missedQueries = operations.filter((_, i) => !metrics[i]?.cacheHit); const uniqueMisses = new Map(); for (const query of missedQueries) { const signature = this.generateCacheKey(query.query); uniqueMisses.set(signature, (uniqueMisses.get(signature) || 0) + 1); } report += '### 🔍 Cache Miss Opportunities\n\n'; const sortedMisses = [...uniqueMisses.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, 5); for (const [signature, count] of sortedMisses) { if (count > 1) { report += `#### Missed ${count} times\n`; const sampleQuery = missedQueries.find(q => this.generateCacheKey(q.query) === signature); if (sampleQuery) { report += `\`\`\`graphql\n${sampleQuery.query.substring(0, 200)}${sampleQuery.query.length > 200 ? '...' : ''}\n\`\`\`\n\n`; } } } } // Performance impact const cachedAvgDuration = metrics.filter(m => m.cacheHit).reduce((sum, m) => sum + m.duration, 0) / cacheHits || 0; const uncachedAvgDuration = metrics.filter(m => !m.cacheHit).reduce((sum, m) => sum + m.duration, 0) / cacheMisses || 0; if (cacheHits > 0 && cacheMisses > 0) { report += '### Performance Impact\n'; report += `- **Cached Queries Avg Duration:** ${cachedAvgDuration.toFixed(2)}ms\n`; report += `- **Uncached Queries Avg Duration:** ${uncachedAvgDuration.toFixed(2)}ms\n`; if (uncachedAvgDuration > cachedAvgDuration) { const speedup = (uncachedAvgDuration / cachedAvgDuration).toFixed(1); report += `- **Cache Speedup:** ${speedup}x faster\n`; } report += '\n'; } // Recommendations report += '### 🎯 Cache Optimization Recommendations\n\n'; if (parseFloat(hitRate) < 30) { report += '1. **Low Hit Rate**: Consider implementing query-level caching\n'; } if (cacheablePatterns.some(p => parseFloat(p.hitRate) < 50 && p.total > 5)) { report += '2. **Repeated Misses**: Some queries are repeated but not cached\n'; } if (uncachedAvgDuration > cachedAvgDuration * 2) { report += '3. **Performance Gains**: Significant performance improvement potential with better caching\n'; } if (operations.some(op => op.type === 'query' && !metrics.find(m => m.cacheHit))) { report += '4. **Query Caching**: Consider implementing field-level or query-level caching\n'; } report += '5. **Cache Strategy**: Review cache TTL and invalidation strategies\n'; return { content: [{ type: 'text', text: report }] }; } getOperationTypeDistribution(operations) { const distribution = {}; for (const op of operations) { distribution[op.type] = (distribution[op.type] || 0) + 1; } return distribution; } evaluateCacheEffectiveness(hitRate) { if (hitRate >= 80) { return { rating: 'Excellent 🎉', description: 'Your cache is working very effectively!' }; } else if (hitRate >= 60) { return { rating: 'Good ✅', description: 'Cache is working well with room for improvement.' }; } else if (hitRate >= 40) { return { rating: 'Fair ⚠️', description: 'Cache is helping but could be optimized further.' }; } else if (hitRate >= 20) { return { rating: 'Poor 🔸', description: 'Cache is not very effective, consider reviewing strategy.' }; } else { return { rating: 'Very Poor 🔴', description: 'Cache is barely helping, needs significant improvement.' }; } } generateCacheKey(query) { // Generate a simple cache key by normalizing the query return query .replace(/\s+/g, ' ') .replace(/\$\w+/g, '$VAR') // Replace variables .trim(); } } //# sourceMappingURL=graphql-handler.js.map