UNPKG

@logspace/mcp-server

Version:

MCP server for Logspace log analysis integration with AI models.

659 lines 28.1 kB
/** * Code Execution with MCP approach for LogSpace * Implements Anthropic's new pattern for efficient MCP usage */ import { getEnhancedApiClient } from '../services/enhancedApiClient.js'; import { validateBugId } from '../utils/validator.js'; /** * LogSpace MCP Code API - TypeScript wrapper functions for code execution */ export class LogSpaceAPI { client = getEnhancedApiClient(); /** * Analyze session and return structured insights */ async analyzeSession(bugId) { const id = validateBugId(bugId); return await this.client.getSessionAnalysis(id); } /** * Get complete session metadata including environment, user, timestamps */ async getSessionMetadata(bugId) { const id = validateBugId(bugId); const fullData = await this.client.fetchLogData(id); return { sessionInfo: { id: fullData.id, username: fullData.username, assignee: fullData.assignee_username, environment: fullData.environment, status: fullData.status, created_at: fullData.created_at, video_url: fullData.video_url, }, metadata: fullData.log_json.metadata, sessionData: fullData.log_json.session, }; } /** * Get performance logs (page load times, resource timings, etc.) */ async getPerformanceLogs(bugId, options = {}) { const id = validateBugId(bugId); const fullData = await this.client.fetchLogData(id); let performanceLogs = fullData.log_json.logs.performance || []; // Apply filters if (options.type) { performanceLogs = performanceLogs.filter((log) => log.type === options.type); } if (options.minDuration) { performanceLogs = performanceLogs.filter((log) => log.duration >= options.minDuration); } if (options.limit) { performanceLogs = performanceLogs.slice(0, options.limit); } return { total: performanceLogs.length, logs: performanceLogs, summary: { averageDuration: performanceLogs.reduce((acc, log) => acc + log.duration, 0) / performanceLogs.length || 0, slowestRequest: Math.max(...performanceLogs.map((log) => log.duration), 0), totalRequests: performanceLogs.length, }, }; } /** * Get user interactions (clicks, inputs, scrolls, navigation) */ async getInteractions(bugId, options = {}) { const id = validateBugId(bugId); const fullData = await this.client.fetchLogData(id); let interactions = fullData.log_json.logs.interaction || []; const sessionStart = fullData.log_json.session.timestamp; // Apply filters if (options.type) { interactions = interactions.filter((interaction) => interaction.type === options.type); } if (options.timeRange) { const [start, end] = options.timeRange; const startTime = sessionStart + start * 1000; const endTime = sessionStart + end * 1000; interactions = interactions.filter((interaction) => interaction.timestamp >= startTime && interaction.timestamp <= endTime); } if (options.beforeTimestamp) { interactions = interactions.filter((interaction) => interaction.timestamp < options.beforeTimestamp); } if (options.limit) { interactions = interactions.slice(0, options.limit); } return { total: interactions.length, interactions: interactions.map((interaction) => ({ ...interaction, relativeTime: (interaction.timestamp - sessionStart) / 1000, // seconds from session start })), summary: { clickCount: interactions.filter((i) => i.type === 'click').length, inputCount: interactions.filter((i) => i.type === 'input').length, scrollCount: interactions.filter((i) => i.type === 'scroll').length, navigationCount: interactions.filter((i) => i.type === 'navigation').length, }, }; } /** * Get annotations (user notes, highlights, issues marked during session) */ async getAnnotations(bugId, options = {}) { const id = validateBugId(bugId); const fullData = await this.client.fetchLogData(id); let annotations = fullData.log_json.logs.annotation || []; // Apply filters if (options.type) { annotations = annotations.filter((annotation) => annotation.type === options.type); } if (options.searchText) { annotations = annotations.filter((annotation) => annotation.text.toLowerCase().includes(options.searchText.toLowerCase())); } if (options.limit) { annotations = annotations.slice(0, options.limit); } return { total: annotations.length, annotations, summary: { noteCount: annotations.filter((a) => a.type === 'note').length, issueCount: annotations.filter((a) => a.type === 'issue').length, highlightCount: annotations.filter((a) => a.type === 'highlight').length, }, }; } /** * Get drawings (if user drew on screen during session) */ async getDrawings(bugId) { const id = validateBugId(bugId); const fullData = await this.client.fetchLogData(id); const drawings = fullData.log_json.logs.drawings || []; return { total: drawings.length, drawings, hasDrawings: drawings.length > 0, }; } /** * Get network requests with flexible filtering */ async getNetworkRequests(bugId, options = {}) { const id = validateBugId(bugId); const filters = { limit: options.limit || 50, include_body: options.includeBodies ?? false, }; if (options.failedOnly) { filters.status_min = 400; } if (options.method) { filters.method = options.method; } if (options.urlPattern) { filters.url_contains = options.urlPattern; } if (options.statusRange) { filters.status_min = options.statusRange[0]; filters.status_max = options.statusRange[1]; } return await this.client.getNetworkRequests(id, filters); } /** * Get console logs with filtering */ async getConsoleLogs(bugId, options = {}) { const id = validateBugId(bugId); const filters = { limit: options.limit || 100, }; if (options.level) { filters.level = options.level; } if (options.search) { filters.search = options.search; } if (options.timeRange) { filters.time_range = `${options.timeRange[0]},${options.timeRange[1]}`; } return await this.client.getConsoleLogs(id, filters); } /** * Get error context with related events */ async getErrorContext(bugId, options = {}) { const id = validateBugId(bugId); return await this.client.getErrorContext(id, { error_index: options.errorIndex || 0, context_window: options.contextWindow || 5, }); } /** * Comprehensive session investigation - returns ALL data types for AI analysis * This gives AI full context instead of pre-filtering patterns */ async investigateSession(bugId, options = {}) { const id = validateBugId(bugId); const { includeFullNetworkBodies = false, maxEventsPerType = 50, timeWindowSeconds } = options; // Get all data types in parallel for comprehensive analysis const [sessionMeta, allNetworkRequests, allConsoleLogs, allInteractions, performanceLogs, annotations, drawings] = await Promise.all([ this.getSessionMetadata(id), this.getNetworkRequests(id, { limit: maxEventsPerType, includeBodies: includeFullNetworkBodies, }), this.getConsoleLogs(id, { limit: maxEventsPerType }), this.getInteractions(id, { limit: maxEventsPerType }), this.getPerformanceLogs(id, { limit: maxEventsPerType }), this.getAnnotations(id, { limit: maxEventsPerType }), this.getDrawings(id), ]); // Build comprehensive timeline if requested let timelineEvents = []; if (timeWindowSeconds) { const sessionStart = sessionMeta.sessionData.timestamp; const timeWindowMs = timeWindowSeconds * 1000; // Combine all events into timeline const allEvents = [ ...allNetworkRequests.requests.map((r) => ({ ...r, eventType: 'network' })), ...allConsoleLogs.logs.map((l) => ({ ...l, eventType: 'console' })), ...allInteractions.interactions.map((i) => ({ ...i, eventType: 'interaction' })), ...performanceLogs.logs.map((p) => ({ ...p, eventType: 'performance' })), ...annotations.annotations.map((a) => ({ ...a, eventType: 'annotation' })), ]; // Sort by timestamp and limit to time window timelineEvents = allEvents .filter((event) => event.timestamp >= sessionStart && event.timestamp <= sessionStart + timeWindowMs) .sort((a, b) => a.timestamp - b.timestamp) .slice(0, 200); // Limit to prevent token overflow } return { sessionInfo: sessionMeta, data: { network: { ...allNetworkRequests, patterns: this.analyzeNetworkPatterns(allNetworkRequests.requests), }, console: { ...allConsoleLogs, patterns: this.analyzeConsolePatterns(allConsoleLogs.logs), }, interactions: { ...allInteractions, patterns: this.analyzeInteractionPatterns(allInteractions.interactions), }, performance: { ...performanceLogs, patterns: this.analyzePerformancePatterns(performanceLogs.logs), }, annotations, drawings, }, timeline: timelineEvents, insights: { potentialIssues: this.identifyPotentialIssues(allNetworkRequests.requests, allConsoleLogs.logs, allInteractions.interactions, performanceLogs.logs), suspiciousSequences: this.findSuspiciousSequences(timelineEvents), missingExpectedActions: this.detectMissingActions(allInteractions.interactions, allNetworkRequests.requests), }, }; } /** * Legacy method for backward compatibility - now calls investigateSession * @deprecated Use investigateSession for more comprehensive analysis */ async findSuspiciousPatterns(bugId) { const investigation = await this.investigateSession(bugId, { maxEventsPerType: 20 }); return { sessionSummary: investigation.sessionInfo, criticalIssues: investigation.insights.potentialIssues, recommendations: this.generateRecommendationsFromInvestigation(investigation), }; } // Pattern analysis methods analyzeNetworkPatterns(requests) { const patterns = { failureRate: 0, averageResponseTime: 0, statusCodeDistribution: {}, timeoutCount: 0, redirectCount: 0, uniqueHosts: new Set(), successfulButSlowRequests: [], identicalFailedRequests: [], }; if (requests.length === 0) return patterns; requests.forEach((req) => { // Status code distribution patterns.statusCodeDistribution[req.status] = (patterns.statusCodeDistribution[req.status] || 0) + 1; // Unique hosts try { patterns.uniqueHosts.add(new URL(req.url).hostname); } catch { } // Slow but successful requests (200 OK but > 3 seconds) if (req.status === 200 && req.duration > 3000) { patterns.successfulButSlowRequests.push(req); } // Timeout detection (duration > 30s usually indicates timeout) if (req.duration > 30000) { patterns.timeoutCount++; } // Redirects if (req.status >= 300 && req.status < 400) { patterns.redirectCount++; } }); patterns.failureRate = requests.filter((r) => r.status >= 400).length / requests.length; patterns.averageResponseTime = requests.reduce((acc, req) => acc + req.duration, 0) / requests.length; // Find identical failed requests (same URL, same failure) const failedRequestGroups = requests .filter((r) => r.status >= 400) .reduce((groups, req) => { const key = `${req.method}_${req.url}_${req.status}`; groups[key] = groups[key] || []; groups[key].push(req); return groups; }, {}); patterns.identicalFailedRequests = Object.values(failedRequestGroups) .filter((group) => group.length > 1) .flat(); return { ...patterns, uniqueHosts: Array.from(patterns.uniqueHosts), }; } analyzeConsolePatterns(logs) { const patterns = { errorFrequency: 0, warningFrequency: 0, errorTypes: {}, repeatedMessages: [], errorClusters: [], }; if (logs.length === 0) return patterns; const errorLogs = logs.filter((log) => log.type === 'error'); const warnLogs = logs.filter((log) => log.type === 'warn'); patterns.errorFrequency = errorLogs.length / logs.length; patterns.warningFrequency = warnLogs.length / logs.length; // Group by message for repeated errors const messageGroups = logs.reduce((groups, log) => { const message = log.message.substring(0, 100); // First 100 chars groups[message] = groups[message] || []; groups[message].push(log); return groups; }, {}); patterns.repeatedMessages = Object.values(messageGroups) .filter((group) => group.length > 2) .map((group) => ({ message: group[0].message, count: group.length, timestamps: group.map((log) => log.timestamp), })); return patterns; } analyzeInteractionPatterns(interactions) { const patterns = { userEngagement: 0, rapidClicking: [], unusualScrolling: [], navigationPattern: [], inputBehavior: [], }; if (interactions.length === 0) return patterns; // Calculate user engagement (interactions per minute) const sessionDuration = interactions.length > 0 ? (interactions[interactions.length - 1].timestamp - interactions[0].timestamp) / 60000 : 0; patterns.userEngagement = sessionDuration > 0 ? interactions.length / sessionDuration : 0; // Detect rapid clicking (multiple clicks within 500ms) const clicks = interactions.filter((i) => i.type === 'click').sort((a, b) => a.timestamp - b.timestamp); for (let i = 1; i < clicks.length; i++) { if (clicks[i].timestamp - clicks[i - 1].timestamp < 500) { patterns.rapidClicking.push({ timestamp: clicks[i].timestamp, element: clicks[i].element, gap: clicks[i].timestamp - clicks[i - 1].timestamp, }); } } // Navigation pattern patterns.navigationPattern = interactions .filter((i) => i.type === 'navigation') .map((nav) => ({ timestamp: nav.timestamp, url: nav.value, relativeTime: nav.relativeTime, })); return patterns; } analyzePerformancePatterns(performanceLogs) { const patterns = { slowestOperations: [], averageLoadTime: 0, performanceBottlenecks: [], }; if (performanceLogs.length === 0) return patterns; patterns.averageLoadTime = performanceLogs.reduce((acc, log) => acc + log.duration, 0) / performanceLogs.length; patterns.slowestOperations = performanceLogs.sort((a, b) => b.duration - a.duration).slice(0, 5); patterns.performanceBottlenecks = performanceLogs .filter((log) => log.duration > patterns.averageLoadTime * 2) .map((log) => ({ name: log.name, duration: log.duration, type: log.type, })); return patterns; } identifyPotentialIssues(networkRequests, consoleLogs, interactions, performanceLogs) { const issues = []; // Network issues const failedRequests = networkRequests.filter((r) => r.status >= 400); if (failedRequests.length > 0) { issues.push({ type: 'network_failures', severity: failedRequests.some((r) => r.status >= 500) ? 'high' : 'medium', count: failedRequests.length, details: failedRequests.slice(0, 3), // First 3 for context }); } // Successful requests with error responses (200 but contains error data) const suspiciousSuccessful = networkRequests.filter((r) => r.status === 200 && r.responseBody && (r.responseBody.includes('error') || r.responseBody.includes('exception'))); if (suspiciousSuccessful.length > 0) { issues.push({ type: 'masked_api_errors', severity: 'medium', count: suspiciousSuccessful.length, details: suspiciousSuccessful.slice(0, 3), }); } // Console errors const consoleErrors = consoleLogs.filter((log) => log.type === 'error'); if (consoleErrors.length > 0) { issues.push({ type: 'javascript_errors', severity: 'high', count: consoleErrors.length, details: consoleErrors.slice(0, 3), }); } // Performance issues const slowOperations = performanceLogs.filter((log) => log.duration > 5000); if (slowOperations.length > 0) { issues.push({ type: 'performance_issues', severity: 'medium', count: slowOperations.length, details: slowOperations.slice(0, 3), }); } // User frustration indicators (rapid clicking) const rapidClicks = this.detectRapidClicks(interactions); if (rapidClicks.length > 0) { issues.push({ type: 'user_frustration', severity: 'medium', count: rapidClicks.length, details: rapidClicks, }); } return issues; } findSuspiciousSequences(timelineEvents) { const sequences = []; // Look for error followed by rapid user actions (indicates user trying to fix something) for (let i = 0; i < timelineEvents.length - 1; i++) { const event = timelineEvents[i]; const nextEvents = timelineEvents.slice(i + 1, i + 4); // Next 3 events if ((event.eventType === 'console' && event.type === 'error') || (event.eventType === 'network' && event.status >= 400)) { const userReactions = nextEvents.filter((e) => e.eventType === 'interaction' && e.timestamp - event.timestamp < 5000); // Within 5 seconds if (userReactions.length >= 2) { sequences.push({ type: 'error_user_reaction', trigger: event, reactions: userReactions, pattern: 'User immediately reacted to error/failure', }); } } } return sequences; } detectMissingActions(interactions, networkRequests) { const missing = []; // Look for network requests without preceding user interactions const userInitiatedTypes = ['click', 'input', 'navigation']; networkRequests.forEach((req) => { // Find interactions in the 2 seconds before this request const precedingInteractions = interactions.filter((interaction) => userInitiatedTypes.includes(interaction.type) && interaction.timestamp < req.timestamp && req.timestamp - interaction.timestamp < 2000); if (precedingInteractions.length === 0 && !req.url.includes('analytics') && !req.url.includes('tracking')) { missing.push({ type: 'unexpected_network_call', request: req, issue: 'Network request made without user interaction', }); } }); return missing; } detectRapidClicks(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++) { if (clicks[i].timestamp - clicks[i - 1].timestamp < 500) { rapidClicks.push({ timestamp: clicks[i].timestamp, element: clicks[i].element, interval: clicks[i].timestamp - clicks[i - 1].timestamp, }); } } return rapidClicks; } generateRecommendationsFromInvestigation(investigation) { const recommendations = []; const issues = investigation.insights.potentialIssues; issues.forEach((issue) => { switch (issue.type) { case 'network_failures': recommendations.push({ type: 'network', priority: issue.severity, message: `Found ${issue.count} failed network requests. Review API endpoints and error handling.`, details: issue.details, }); break; case 'masked_api_errors': recommendations.push({ type: 'api_design', priority: 'high', message: `Found ${issue.count} requests returning 200 OK but containing error data. This masks real failures.`, details: issue.details, }); break; case 'user_frustration': recommendations.push({ type: 'ux', priority: 'medium', message: `User showed frustration indicators (rapid clicking). Check UI responsiveness and feedback.`, details: issue.details, }); break; } }); return recommendations; } /** * Helper: Debug error chain */ async debugErrorChain(bugId, errorIndex = 0) { const errorContext = await this.getErrorContext(bugId, { errorIndex, contextWindow: 10, }); // Get related network failures const relatedNetworkFailures = errorContext.related_events.network.filter((req) => req.status >= 400); // Get console logs around error time const relatedConsoleLogs = errorContext.related_events.console.filter((log) => ['error', 'warn'].includes(log.type)); return { primaryError: errorContext.error, timeline: { networkFailures: relatedNetworkFailures, consoleLogs: relatedConsoleLogs, userActions: errorContext.related_events.interactions, }, diagnosis: this.diagnoseError(errorContext), }; } generateRecommendations(analysis, failedRequests, consoleErrors) { const recommendations = []; if (failedRequests.requests.length > 0) { recommendations.push({ type: 'network', priority: 'high', message: `Found ${failedRequests.requests.length} failed network requests. Check API endpoints and authentication.`, }); } if (consoleErrors.logs.length > 0) { recommendations.push({ type: 'javascript', priority: 'high', message: `Found ${consoleErrors.logs.length} JavaScript errors. Review error messages and stack traces.`, }); } if (analysis.performance.slowest_request > 5000) { recommendations.push({ type: 'performance', priority: 'medium', message: `Slowest request took ${analysis.performance.slowest_request}ms. Consider optimizing slow endpoints.`, }); } return recommendations; } diagnoseError(errorContext) { const error = errorContext.error; const relatedEvents = errorContext.related_events; // Basic error classification let category = 'unknown'; let severity = 'medium'; if (error.message.includes('network') || error.message.includes('fetch')) { category = 'network'; severity = 'high'; } else if (error.message.includes('undefined') || error.message.includes('null')) { category = 'reference'; severity = 'high'; } else if (error.message.includes('permission') || error.message.includes('auth')) { category = 'authentication'; severity = 'high'; } return { category, severity, likelyCause: this.inferCause(error, relatedEvents), suggestedFix: this.suggestFix(category, error), }; } inferCause(error, relatedEvents) { // Simple cause inference based on context if (relatedEvents.network.some((req) => req.status >= 500)) { return 'Server error occurred before this JavaScript error'; } if (relatedEvents.network.some((req) => req.status === 401 || req.status === 403)) { return 'Authentication/authorization failure may have caused this error'; } if (relatedEvents.interactions.length > 0) { return 'Error occurred after user interaction, possibly due to invalid state'; } return 'Error appears to be isolated, check application logic'; } suggestFix(category, error) { switch (category) { case 'network': return 'Check network connectivity, API endpoints, and error handling for failed requests'; case 'reference': return 'Add null checks and validate object properties before access'; case 'authentication': return 'Verify authentication tokens and permission levels'; default: return 'Review error stack trace and add appropriate error handling'; } } } // Export singleton instance for code execution export const logspace = new LogSpaceAPI(); //# sourceMappingURL=codeExecutionAPI.js.map