@logspace/mcp-server
Version:
MCP server for Logspace log analysis integration with AI models.
405 lines • 16.2 kB
JavaScript
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