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