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