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

295 lines • 13.2 kB
export class DatabaseTraceEngine { page; queries = []; queryPatterns = { prisma: { detect: /__prisma|prisma\$|PrismaClient/, operations: ['findMany', 'findFirst', 'findUnique', 'create', 'update', 'delete', 'upsert', 'count', 'aggregate'] }, typeorm: { detect: /typeorm|EntityManager|Repository/, operations: ['find', 'findOne', 'save', 'remove', 'delete', 'insert', 'update', 'query'] }, sequelize: { detect: /sequelize|Sequelize/, operations: ['findAll', 'findOne', 'create', 'update', 'destroy', 'bulkCreate'] }, mongoose: { detect: /mongoose|Mongoose|Schema/, operations: ['find', 'findOne', 'findById', 'create', 'updateOne', 'deleteOne', 'aggregate'] } }; async attach(page) { this.page = page; // Inject database query interceptor await page.addInitScript(() => { window.__DB_QUERIES__ = []; // Prisma interceptor if (window.Prisma || global.Prisma) { const PrismaConstructor = window.Prisma || global.Prisma; const originalClient = PrismaConstructor.PrismaClient; if (originalClient) { PrismaConstructor.PrismaClient = class extends originalClient { constructor(...args) { super(...args); // Wrap all Prisma operations const models = Object.keys(this).filter(key => typeof this[key] === 'object' && !key.startsWith('_') && !key.startsWith('$')); models.forEach(model => { const modelObj = this[model]; ['findMany', 'findFirst', 'findUnique', 'create', 'update', 'delete', 'upsert', 'count'].forEach(op => { if (modelObj[op]) { const original = modelObj[op].bind(modelObj); modelObj[op] = async (args) => { const startTime = Date.now(); const query = { id: `prisma_${Date.now()}_${Math.random()}`, timestamp: new Date(), orm: 'prisma', operation: op, table: model, params: args, stackTrace: new Error().stack?.split('\n').slice(2, 7) }; try { const result = await original(args); window.__DB_QUERIES__.push({ ...query, duration: Date.now() - startTime, result: result }); return result; } catch (error) { window.__DB_QUERIES__.push({ ...query, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error) }); throw error; } }; } }); }); } }; } } // Generic fetch interceptor for API routes that might hit database const originalFetch = window.fetch; window.fetch = async (...args) => { const [url, options] = args; const isApiRoute = typeof url === 'string' && (url.includes('/api/') || url.includes('/graphql') || url.includes('/_next/data/')); if (isApiRoute) { const startTime = Date.now(); const query = { id: `api_${Date.now()}_${Math.random()}`, timestamp: new Date(), orm: 'api', operation: options?.method || 'GET', query: url.toString(), params: options?.body ? JSON.parse(options.body) : undefined }; try { const response = await originalFetch(...args); const duration = Date.now() - startTime; // Clone response to read body const clonedResponse = response.clone(); const body = await clonedResponse.json().catch(() => null); window.__DB_QUERIES__.push({ ...query, duration, result: body }); return response; } catch (error) { window.__DB_QUERIES__.push({ ...query, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error) }); throw error; } } return originalFetch(...args); }; }); // Server-side interceptor for Next.js API routes await page.route('**/_next/data/**', async (route) => { const request = route.request(); const startTime = Date.now(); // Continue the request const response = await route.fetch(); // Capture the response if (response) { try { const body = await response.json(); const duration = Date.now() - startTime; // Look for database query patterns in response if (this.looksLikeDatabaseResult(body)) { this.queries.push({ id: `ssr_${Date.now()}_${Math.random()}`, timestamp: new Date(), query: request.url(), duration, result: body, orm: 'next-ssr', operation: 'getData' }); } } catch (e) { // Not JSON response } } await route.fulfill({ response }); }); } looksLikeDatabaseResult(data) { if (!data) return false; // Check if it's an array of objects (common DB result) if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object') { return true; } // Check for common DB result patterns if (typeof data === 'object') { const keys = Object.keys(data); const dbPatterns = ['id', 'createdAt', 'updatedAt', 'data', 'rows', 'count', 'results']; return dbPatterns.some(pattern => keys.includes(pattern)); } return false; } async captureQueries(duration) { if (!this.page) throw new Error('No page attached'); const startCount = this.queries.length; await this.page.waitForTimeout(duration); // Get queries from the page const pageQueries = await this.page.evaluate(() => { return window.__DB_QUERIES__ || []; }); // Add page queries to our collection pageQueries.forEach((q) => { this.queries.push({ ...q, timestamp: new Date(q.timestamp) }); }); // Clear page queries await this.page.evaluate(() => { window.__DB_QUERIES__ = []; }); return this.queries.slice(startCount); } getQueries(filter) { let queries = [...this.queries]; if (filter?.minDuration) { queries = queries.filter(q => q.duration && q.duration >= filter.minDuration); } if (filter?.operation) { queries = queries.filter(q => q.operation === filter.operation); } if (filter?.hasError !== undefined) { queries = queries.filter(q => filter.hasError ? !!q.error : !q.error); } if (filter?.table) { queries = queries.filter(q => q.table === filter.table); } return queries; } detectN1Queries() { const n1Patterns = []; // Group queries by table and operation within a time window const timeWindow = 1000; // 1 second const grouped = new Map(); this.queries.forEach(query => { if (query.table && query.operation) { const key = `${query.table}_${query.operation}`; if (!grouped.has(key)) { grouped.set(key, []); } grouped.get(key).push(query); } }); // Detect N+1 patterns grouped.forEach((queries, key) => { // Sort by timestamp queries.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); // Look for repeated queries in close succession for (let i = 0; i < queries.length - 1; i++) { const current = queries[i]; const next = queries[i + 1]; const timeDiff = next.timestamp.getTime() - current.timestamp.getTime(); if (timeDiff <= timeWindow) { // Check if it's the same operation on same table if (current.operation === next.operation && current.table === next.table) { // Collect all queries in this burst const burst = [current, next]; let j = i + 2; while (j < queries.length && queries[j].timestamp.getTime() - queries[j - 1].timestamp.getTime() <= timeWindow) { burst.push(queries[j]); j++; } if (burst.length > 2) { n1Patterns.push({ pattern: `N+1 query detected: ${burst.length} ${current.operation} queries on ${current.table}`, queries: burst, suggestion: `Consider using a single query with includes/joins instead of ${burst.length} separate queries` }); i = j - 1; // Skip processed queries } } } } }); return n1Patterns; } getSlowQueries(thresholdMs = 100) { return this.queries.filter(q => q.duration && q.duration > thresholdMs); } getFailedQueries() { return this.queries.filter(q => !!q.error); } getQueryStats() { const stats = { total: this.queries.length, byOperation: {}, byTable: {}, avgDuration: 0, slowest: null }; let totalDuration = 0; let maxDuration = 0; this.queries.forEach(query => { // By operation if (query.operation) { stats.byOperation[query.operation] = (stats.byOperation[query.operation] || 0) + 1; } // By table if (query.table) { stats.byTable[query.table] = (stats.byTable[query.table] || 0) + 1; } // Duration stats if (query.duration) { totalDuration += query.duration; if (query.duration > maxDuration) { maxDuration = query.duration; stats.slowest = query; } } }); stats.avgDuration = this.queries.length > 0 ? Math.round(totalDuration / this.queries.length) : 0; return stats; } clearQueries() { this.queries = []; } } //# sourceMappingURL=database-trace-engine.js.map