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

334 lines • 12.7 kB
export class GraphQLExtractionUtils { /** * Extract GraphQL operations from network requests */ static extractGraphQLOperations(requests) { const operations = []; for (const request of requests) { // Look for GraphQL endpoints if (!this.isGraphQLRequest(request)) { continue; } try { const operation = this.parseGraphQLRequest(request); if (operation) { operations.push(operation); } } catch (error) { // Skip malformed requests console.warn('Failed to parse GraphQL request:', error); } } return operations; } /** * Extract GraphQL errors from operations */ static extractGraphQLErrors(requests) { const errors = []; const operations = this.extractGraphQLOperations(requests); for (const operation of operations) { if (operation.errors && operation.errors.length > 0) { for (const error of operation.errors) { errors.push({ message: error.message || 'Unknown GraphQL error', locations: error.locations, path: error.path, extensions: error.extensions, timestamp: operation.timestamp, operation: operation.name || operation.type }); } } } return errors; } /** * Analyze query performance and detect issues */ static analyzeQueryPerformance(requests) { const metrics = []; const operations = this.extractGraphQLOperations(requests); for (const operation of operations) { if (operation.duration !== undefined) { const metric = { operation: operation.name || `${operation.type}_${operation.timestamp.getTime()}`, duration: operation.duration, timestamp: operation.timestamp, complexity: operation.performance?.complexity, cacheHit: this.detectCacheHit(operation.response) }; // Extract resolver stats from response extensions if (operation.response?.extensions?.tracing) { metric.resolverStats = this.parseResolverStats(operation.response.extensions.tracing); } metrics.push(metric); } } return metrics; } /** * Detect N+1 query patterns */ static detectNPlusOneQueries(requests) { const operations = this.extractGraphQLOperations(requests); const patterns = new Map(); // Group similar operations for (const operation of operations) { const signature = this.generateQuerySignature(operation.query); if (!patterns.has(signature)) { patterns.set(signature, []); } patterns.get(signature).push(operation); } const nPlusOnePatterns = []; // Look for repeated similar queries in short timeframes for (const [signature, ops] of patterns) { if (ops.length >= 3) { // Check if operations are clustered in time (potential N+1) const timeSpan = Math.max(...ops.map(op => op.timestamp.getTime())) - Math.min(...ops.map(op => op.timestamp.getTime())); if (timeSpan < 5000) { // Within 5 seconds nPlusOnePatterns.push({ pattern: signature, occurrences: ops.length, operations: ops, suggestion: this.generateNPlusOneSuggestion(ops[0]) }); } } } return nPlusOnePatterns; } /** * Analyze schema usage patterns */ static analyzeSchemaUsage(requests) { const usage = new Map(); const operations = this.extractGraphQLOperations(requests); for (const operation of operations) { const fields = this.extractFieldsFromQuery(operation.query); for (const field of fields) { const key = `${field.type}.${field.name}`; if (!usage.has(key)) { usage.set(key, { field: field.name, type: field.type, usageCount: 0, lastUsed: operation.timestamp, deprecated: field.deprecated }); } const fieldUsage = usage.get(key); fieldUsage.usageCount++; if (operation.timestamp > fieldUsage.lastUsed) { fieldUsage.lastUsed = operation.timestamp; } } } return Array.from(usage.values()); } /** * Calculate query complexity metrics */ static calculateQueryComplexity(query) { const warnings = []; let depth = 0; let fieldCount = 0; let complexity = 0; try { // Simple heuristic-based analysis (could be enhanced with full GraphQL parsing) const lines = query.split('\n'); let currentDepth = 0; for (const line of lines) { const trimmed = line.trim(); // Count opening braces for depth const openBraces = (trimmed.match(/{/g) || []).length; const closeBraces = (trimmed.match(/}/g) || []).length; currentDepth += openBraces - closeBraces; depth = Math.max(depth, currentDepth); // Count fields (simple heuristic) if (trimmed && !trimmed.startsWith('#') && !trimmed.includes('{') && !trimmed.includes('}')) { fieldCount++; } } // Calculate complexity score complexity = fieldCount + (depth * 2); // Generate warnings if (depth > 10) { warnings.push(`Query depth of ${depth} may be too deep`); } if (fieldCount > 50) { warnings.push(`Query selects ${fieldCount} fields, consider reducing selection`); } if (complexity > 100) { warnings.push(`Query complexity score of ${complexity} is very high`); } } catch (error) { warnings.push('Failed to analyze query complexity'); } return { depth, fieldCount, complexity, warnings }; } /** * Check if request is a GraphQL request */ static isGraphQLRequest(request) { // Check URL patterns if (request.url.includes('/graphql') || request.url.includes('/graphiql')) { return true; } // Check content type const contentType = request.headers?.['Content-Type'] || request.headers?.['content-type'] || ''; if (contentType.includes('application/json') && request.method === 'POST') { // Check if body contains GraphQL query try { const body = JSON.parse(request.requestBody || '{}'); return body.query && typeof body.query === 'string'; } catch { return false; } } return false; } /** * Parse GraphQL request into operation object */ static parseGraphQLRequest(request) { try { const body = JSON.parse(request.requestBody || '{}'); const response = request.responseBody ? JSON.parse(request.responseBody) : null; const operation = { type: this.detectOperationType(body.query), name: body.operationName, query: body.query, variables: body.variables, timestamp: request.timestamp, duration: request.duration, response, errors: response?.errors }; // Enhance with performance data operation.performance = this.calculateQueryComplexity(body.query); // Detect client information operation.client = this.detectGraphQLClient(request); return operation; } catch (error) { return null; } } /** * Detect GraphQL operation type from query string */ static detectOperationType(query) { if (!query) return 'unknown'; const trimmed = query.trim().toLowerCase(); if (trimmed.startsWith('mutation')) return 'mutation'; if (trimmed.startsWith('subscription')) return 'subscription'; if (trimmed.startsWith('query') || trimmed.startsWith('{')) return 'query'; return 'unknown'; } /** * Detect GraphQL client from request headers/body */ static detectGraphQLClient(request) { const userAgent = request.headers?.['User-Agent'] || request.headers?.['user-agent'] || ''; const apolloClientName = request.headers?.['apollographql-client-name']; const apolloClientVersion = request.headers?.['apollographql-client-version']; if (apolloClientName) { return { name: apolloClientName, version: apolloClientVersion }; } if (userAgent.includes('Apollo')) { return { name: 'Apollo Client' }; } if (userAgent.includes('Relay')) { return { name: 'Relay' }; } return {}; } /** * Generate query signature for pattern detection */ static generateQuerySignature(query) { // Remove variables and whitespace to create signature return query .replace(/\$\w+:\s*\w+/g, '$VAR') // Replace variable declarations .replace(/\$\w+/g, '$VAR') // Replace variable usages .replace(/:\s*"[^"]*"/g, ': "VALUE"') // Replace string values .replace(/:\s*\d+/g, ': NUM') // Replace numbers .replace(/\s+/g, ' ') // Normalize whitespace .trim(); } /** * Generate N+1 query suggestion */ static generateNPlusOneSuggestion(operation) { const type = operation.type; if (type === 'query') { return 'Consider using dataloader or adding required fields to parent query to avoid N+1 pattern'; } return 'Multiple similar operations detected - consider batching or caching'; } /** * Extract fields from GraphQL query (simplified) */ static extractFieldsFromQuery(query) { const fields = []; // Simple field extraction (could be enhanced with proper GraphQL parsing) const lines = query.split('\n'); let currentType = 'Query'; for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#') && !trimmed.includes('{') && !trimmed.includes('}')) { const fieldMatch = trimmed.match(/(\w+)/); if (fieldMatch) { fields.push({ name: fieldMatch[1], type: currentType, deprecated: trimmed.includes('@deprecated') }); } } } return fields; } /** * Detect cache hit from response */ static detectCacheHit(response) { if (!response) return false; // Check common cache indicators if (response.extensions?.cacheControl) return true; if (response.extensions?.cache?.hit) return true; return false; } /** * Parse resolver statistics from tracing data */ static parseResolverStats(tracing) { const stats = {}; if (tracing.execution?.resolvers) { for (const resolver of tracing.execution.resolvers) { const name = `${resolver.parentType}.${resolver.fieldName}`; if (!stats[name]) { stats[name] = { count: 0, totalTime: 0, avgTime: 0 }; } stats[name].count++; stats[name].totalTime += resolver.duration || 0; stats[name].avgTime = stats[name].totalTime / stats[name].count; } } return stats; } } //# sourceMappingURL=graphql-utils.js.map