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