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
JavaScript
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