UNPKG

@logspace/mcp-server

Version:

MCP server for Logspace log analysis integration with AI models.

405 lines 16.2 kB
import { z } from 'zod'; import { getApiClient } from '../services/apiClient.js'; import { getLogParser } from '../services/logParser.js'; import { formatError, logError, logInfo, isAuthError } from '../utils/errorHandler.js'; import { validateBugId } from '../utils/validator.js'; export const SmartAnalysisSchema = z.object({ bugId: z.union([z.number(), z.string()]).describe('The ID of the bug/session to analyze'), depth: z .enum(['quick', 'standard', 'comprehensive']) .optional() .default('standard') .describe('Analysis depth: quick (fast scan), standard (balanced), comprehensive (detailed)'), }); export async function smartAnalysis(args) { const startTime = Date.now(); try { const bugId = validateBugId(args.bugId); const depth = args.depth || 'standard'; logInfo(`Starting smart analysis for bug ID: ${bugId} with depth: ${depth}`); const apiClient = getApiClient(); const logData = await apiClient.fetchLogData(bugId); const logParser = getLogParser(); // Run analysis based on depth let result; switch (depth) { case 'quick': result = await quickAnalysis(logData, logParser); break; case 'comprehensive': result = await comprehensiveAnalysis(logData, logParser); break; case 'standard': default: result = await standardAnalysis(logData, logParser); break; } result.executionTime = Date.now() - startTime; return { content: [ { type: 'text', text: JSON.stringify({ success: true, ...result, }, null, 2), }, ], }; } catch (error) { logError('smart_analysis', error); // Check if this is an authentication error const authErrorMessage = isAuthError(error); if (authErrorMessage) { return { content: [ { type: 'text', text: authErrorMessage, }, ], isError: true, }; } return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: formatError(error), executionTime: Date.now() - startTime, }, null, 2), }, ], isError: true, }; } } async function quickAnalysis(logData, logParser) { const { logs, session } = logData.log_json; // Quick scan for critical issues only const errors = logs.error || []; const networkLogs = logs.network || []; const failedRequests = networkLogs.filter((req) => req.status >= 400); const criticalIssues = []; if (errors.length > 0) { criticalIssues.push({ type: 'javascript_errors', severity: 'high', count: errors.length, summary: `Found ${errors.length} JavaScript error(s)`, examples: errors.slice(0, 2), }); } if (failedRequests.length > 0) { criticalIssues.push({ type: 'network_failures', severity: failedRequests.some((r) => r.status >= 500) ? 'critical' : 'high', count: failedRequests.length, summary: `Found ${failedRequests.length} failed network request(s)`, examples: failedRequests.slice(0, 2).map((r) => ({ method: r.method, url: r.url, status: r.status, })), }); } return { strategy: 'quick', executionTime: 0, issuesFound: criticalIssues.length, criticalIssues, patterns: { suspiciousSequences: [], userFrustrationIndicators: [], performanceBottlenecks: [], }, recommendations: generateRecommendations(criticalIssues, 'quick'), sessionSummary: buildSessionSummary(logData), }; } async function standardAnalysis(logData, logParser) { const { logs } = logData.log_json; const analysis = logParser.analyzeSession(logData); const errors = logs.error || []; const networkLogs = logs.network || []; const consoleLogs = logs.console || []; const interactions = logs.interaction || []; const failedRequests = networkLogs.filter((req) => req.status >= 400); const consoleErrors = consoleLogs.filter((log) => log.type === 'error'); const consoleWarnings = consoleLogs.filter((log) => log.type === 'warn'); const criticalIssues = []; // Analyze JavaScript errors if (errors.length > 0) { criticalIssues.push({ type: 'javascript_errors', severity: 'high', count: errors.length, summary: `Found ${errors.length} JavaScript error(s)`, examples: errors.slice(0, 3), }); } // Analyze network failures if (failedRequests.length > 0) { const has5xx = failedRequests.some((r) => r.status >= 500); const has4xx = failedRequests.some((r) => r.status >= 400 && r.status < 500); criticalIssues.push({ type: 'network_failures', severity: has5xx ? 'critical' : has4xx ? 'high' : 'medium', count: failedRequests.length, summary: `Found ${failedRequests.length} failed network request(s) - ${has5xx ? 'Server errors' : 'Client errors'}`, examples: failedRequests.slice(0, 3).map((r) => ({ method: r.method, url: r.url, status: r.status, duration: r.duration, })), }); } // Analyze console issues if (consoleErrors.length > 0) { criticalIssues.push({ type: 'console_errors', severity: 'medium', count: consoleErrors.length, summary: `Found ${consoleErrors.length} console error(s)`, examples: consoleErrors.slice(0, 2), }); } if (consoleWarnings.length > 5) { criticalIssues.push({ type: 'console_warnings', severity: 'low', count: consoleWarnings.length, summary: `Found ${consoleWarnings.length} console warning(s)`, examples: consoleWarnings.slice(0, 2), }); } // Detect patterns const patterns = { suspiciousSequences: findSuspiciousSequences(analysis.timeline), userFrustrationIndicators: detectUserFrustration(interactions), performanceBottlenecks: detectPerformanceIssues(networkLogs), }; return { strategy: 'standard', executionTime: 0, issuesFound: criticalIssues.length, criticalIssues, patterns, recommendations: generateRecommendations(criticalIssues, 'standard'), sessionSummary: buildSessionSummary(logData), }; } async function comprehensiveAnalysis(logData, logParser) { // Start with standard analysis const standardResult = await standardAnalysis(logData, logParser); const { logs } = logData.log_json; const networkLogs = logs.network || []; const consoleLogs = logs.console || []; const interactions = logs.interaction || []; // Additional comprehensive checks const additionalIssues = []; // Check for masked errors (200 OK but contains error data) const maskedErrors = networkLogs.filter((req) => { if (req.status !== 200) return false; const responseBody = (req.responseBody || '').toLowerCase(); return (responseBody.includes('error') || responseBody.includes('exception') || responseBody.includes('"success":false') || responseBody.includes('"status":"error"')); }); if (maskedErrors.length > 0) { additionalIssues.push({ type: 'masked_api_errors', severity: 'high', count: maskedErrors.length, summary: `Found ${maskedErrors.length} request(s) returning 200 OK but containing error data`, examples: maskedErrors.slice(0, 2).map((r) => ({ method: r.method, url: r.url, responsePreview: (r.responseBody || '').substring(0, 200), })), }); } // Check for slow requests (even successful ones) const slowRequests = networkLogs.filter((req) => req.duration > 3000 && req.status < 400); if (slowRequests.length > 0) { additionalIssues.push({ type: 'slow_requests', severity: 'medium', count: slowRequests.length, summary: `Found ${slowRequests.length} slow request(s) (>3s)`, examples: slowRequests.slice(0, 3).map((r) => ({ method: r.method, url: r.url, status: r.status, duration: `${r.duration}ms`, })), }); } // Detect missing API calls (user action without follow-up request) const missingApiCalls = detectMissingApiCalls(interactions, networkLogs); if (missingApiCalls.length > 0) { additionalIssues.push({ type: 'missing_api_calls', severity: 'medium', count: missingApiCalls.length, summary: `Found ${missingApiCalls.length} user action(s) without expected API call`, examples: missingApiCalls.slice(0, 2), }); } return { ...standardResult, strategy: 'comprehensive', criticalIssues: [...standardResult.criticalIssues, ...additionalIssues], issuesFound: standardResult.issuesFound + additionalIssues.length, recommendations: generateRecommendations([...standardResult.criticalIssues, ...additionalIssues], 'comprehensive'), }; } function findSuspiciousSequences(timeline) { const sequences = []; // Look for error followed by rapid user actions for (let i = 0; i < timeline.length - 2; i++) { const event = timeline[i]; const nextEvents = timeline.slice(i + 1, i + 4); if (event.type === 'error' || (event.type === 'network' && event.data?.status >= 400)) { const userReactions = nextEvents.filter((e) => e.type === 'interaction' && e.timestamp - event.timestamp < 5000); if (userReactions.length >= 2) { sequences.push({ trigger: { type: event.type, description: event.description, timestamp: event.timestamp, }, reactions: userReactions.length, pattern: 'User immediately reacted to error/failure', }); } } } return sequences.slice(0, 5); } function detectUserFrustration(interactions) { const clicks = interactions.filter((i) => i.type === 'click').sort((a, b) => a.timestamp - b.timestamp); const rapidClicks = []; for (let i = 1; i < clicks.length; i++) { const timeDiff = clicks[i].timestamp - clicks[i - 1].timestamp; if (timeDiff < 500) { rapidClicks.push({ timestamp: clicks[i].timestamp, element: clicks[i].element, interval: timeDiff, indicator: 'Rapid clicking (potential frustration)', }); } } return rapidClicks.slice(0, 5); } function detectPerformanceIssues(networkLogs) { const slowRequests = networkLogs .filter((req) => req.duration > 2000) .sort((a, b) => b.duration - a.duration) .slice(0, 5) .map((req) => ({ url: req.url, method: req.method, duration: `${req.duration}ms`, status: req.status, })); return slowRequests; } function detectMissingApiCalls(interactions, networkLogs) { const missing = []; const actionTypes = ['click', 'submit']; interactions .filter((i) => actionTypes.includes(i.type)) .forEach((interaction) => { // Look for API calls within 3 seconds after interaction const followingRequests = networkLogs.filter((req) => req.timestamp > interaction.timestamp && req.timestamp - interaction.timestamp < 3000); // If button/submit action but no API call const element = interaction.element || ''; if (followingRequests.length === 0 && (element.includes('button') || element.includes('submit'))) { missing.push({ action: interaction.type, element: interaction.element, timestamp: interaction.timestamp, issue: 'Expected API call did not occur', }); } }); return missing; } function generateRecommendations(issues, depth) { const recommendations = []; issues.forEach((issue) => { switch (issue.type) { case 'javascript_errors': recommendations.push({ priority: 'high', category: 'Error Handling', message: `${issue.count} JavaScript error(s) detected`, actionable: 'Review error stack traces and add proper error handling', }); break; case 'network_failures': recommendations.push({ priority: issue.severity === 'critical' ? 'high' : 'medium', category: 'API Integration', message: `${issue.count} failed network request(s)`, actionable: 'Check API endpoints, authentication, and error responses', }); break; case 'masked_api_errors': recommendations.push({ priority: 'high', category: 'API Design', message: `${issue.count} API(s) returning 200 OK with error data`, actionable: 'Fix API responses to use proper HTTP status codes (4xx/5xx)', }); break; case 'slow_requests': recommendations.push({ priority: 'medium', category: 'Performance', message: `${issue.count} slow request(s) detected (>3s)`, actionable: 'Optimize slow endpoints or add loading indicators', }); break; case 'missing_api_calls': recommendations.push({ priority: 'medium', category: 'UX/Integration', message: `${issue.count} user action(s) without expected API calls`, actionable: 'Check event handlers and ensure API calls are triggered', }); break; case 'console_warnings': recommendations.push({ priority: 'low', category: 'Code Quality', message: `${issue.count} console warning(s)`, actionable: 'Review and fix console warnings to improve code quality', }); break; } }); return recommendations; } function buildSessionSummary(logData) { const { logs, session } = logData.log_json; const networkLogs = logs.network || []; const durationMin = Math.floor(session.duration / 60000); const durationSec = Math.floor((session.duration % 60000) / 1000); return { duration: `${durationMin}m ${durationSec}s`, totalErrors: (logs.error || []).length, failedRequests: networkLogs.filter((req) => req.status >= 400).length, successfulRequests: networkLogs.filter((req) => req.status < 400).length, userInteractions: (logs.interaction || []).length, }; } //# sourceMappingURL=smartAnalysis.js.map