@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,264 lines (1,256 loc) • 49.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.UXMetricsCollector = void 0;
exports.collectUXMetrics = collectUXMetrics;
exports.generateUXReport = generateUXReport;
const events_1 = require("events");
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
class UXMetricsCollector extends events_1.EventEmitter {
constructor(config = {}) {
super();
this.currentSession = null;
this.recommendationToAction = (rec) => ({
id: rec.id,
title: rec.title,
description: rec.description,
owner: rec.implementation.steps[0]?.owner || 'Product Team',
timeline: rec.implementation.timeline,
success: rec.successCriteria,
dependencies: rec.implementation.dependencies
});
this.config = {
enableCollection: true,
anonymizeData: true,
collectionInterval: 60000,
storageLocation: path.join(os.homedir(), '.re-shell', 'metrics'),
maxStorageSize: 50 * 1024 * 1024, // 50MB
retentionDays: 30,
enableFeedback: true,
analytics: {
trackCommands: true,
trackErrors: true,
trackPerformance: true,
trackFeatureUsage: true,
trackUserJourney: true
},
feedbackPrompts: [
{
trigger: 'command_completion',
frequency: 50, // Every 50 commands
questions: [
{
id: 'satisfaction',
type: 'rating',
question: 'How satisfied were you with this command?',
scale: { min: 1, max: 5, labels: ['Very Poor', 'Poor', 'Fair', 'Good', 'Excellent'] },
required: true
}
]
},
{
trigger: 'error_occurrence',
frequency: 1, // Every error
conditions: [
{ type: 'error_count', operator: 'gte', value: 3 }
],
questions: [
{
id: 'error_clarity',
type: 'rating',
question: 'How clear was the error message?',
scale: { min: 1, max: 5 },
required: true
},
{
id: 'error_solution',
type: 'text',
question: 'What would have helped you resolve this error faster?',
required: false
}
]
}
],
...config
};
this.storageLocation = this.config.storageLocation;
this.userId = this.generateUserId();
this.initializeStorage();
}
generateUserId() {
// Generate anonymous user ID based on system characteristics
const systemInfo = `${os.hostname()}-${os.platform()}-${os.arch()}`;
return require('crypto').createHash('sha256').update(systemInfo).digest('hex').substring(0, 16);
}
async initializeStorage() {
try {
await fs.ensureDir(this.storageLocation);
await this.cleanupOldData();
}
catch (error) {
this.emit('error', error);
}
}
async cleanupOldData() {
if (!this.config.retentionDays)
return;
try {
const files = await fs.readdir(this.storageLocation);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
for (const file of files) {
const filePath = path.join(this.storageLocation, file);
const stats = await fs.stat(filePath);
if (stats.mtime < cutoffDate) {
await fs.remove(filePath);
}
}
}
catch (error) {
this.emit('error', error);
}
}
async startSession() {
if (!this.config.enableCollection) {
return this.createMockSession();
}
const sessionId = this.generateSessionId();
const session = {
sessionId,
userId: this.userId,
startTime: new Date(),
platform: os.platform(),
nodeVersion: process.version,
cliVersion: await this.getCLIVersion(),
commands: [],
errors: [],
performance: {
startupTime: 0,
memoryUsage: [],
cpuUsage: [],
commandLatencies: [],
averageResponseTime: 0,
peakMemoryUsage: 0,
totalCommands: 0,
errorRate: 0
},
userAgent: await this.collectUserAgent(),
context: await this.collectSessionContext()
};
this.currentSession = session;
this.emit('session:start', session);
// Start performance monitoring
this.startPerformanceMonitoring();
return session;
}
async endSession() {
if (!this.currentSession)
return;
this.currentSession.endTime = new Date();
this.currentSession.duration = this.currentSession.endTime.getTime() - this.currentSession.startTime.getTime();
// Calculate final performance metrics
this.calculateSessionPerformance();
// Save session data
await this.saveSessionData(this.currentSession);
// Check for feedback prompts
if (this.config.enableFeedback) {
await this.checkFeedbackPrompts('session_end');
}
this.emit('session:end', this.currentSession);
this.currentSession = null;
}
async trackCommand(command, args = [], flags = {}) {
if (!this.config.analytics?.trackCommands || !this.currentSession) {
return '';
}
const commandId = this.generateCommandId();
const startTime = new Date();
const commandMetric = {
commandId,
command,
args,
flags,
startTime,
endTime: startTime,
duration: 0,
success: false,
exitCode: 0,
performance: {
executionTime: 0,
memoryDelta: 0,
cpuTime: 0,
ioOperations: 0,
networkRequests: 0,
cacheHits: 0,
cacheMisses: 0
},
context: {
workingDirectory: process.cwd(),
argumentCount: args.length,
flagCount: Object.keys(flags).length,
isHelp: args.includes('--help') || args.includes('-h'),
isVersion: args.includes('--version') || args.includes('-v'),
hasOutput: false,
outputLength: 0,
interactive: process.stdin.isTTY
}
};
this.currentSession.commands.push(commandMetric);
this.emit('command:start', commandMetric);
return commandId;
}
async trackCommandCompletion(commandId, success, exitCode = 0, error) {
if (!this.currentSession)
return;
const commandMetric = this.currentSession.commands.find(c => c.commandId === commandId);
if (!commandMetric)
return;
commandMetric.endTime = new Date();
commandMetric.duration = commandMetric.endTime.getTime() - commandMetric.startTime.getTime();
commandMetric.success = success;
commandMetric.exitCode = exitCode;
commandMetric.error = error;
// Update performance metrics
this.currentSession.performance.commandLatencies.push(commandMetric.duration);
this.currentSession.performance.totalCommands++;
if (!success) {
this.currentSession.performance.errorRate =
(this.currentSession.performance.errorRate * (this.currentSession.performance.totalCommands - 1) + 1) /
this.currentSession.performance.totalCommands;
}
this.emit('command:complete', commandMetric);
// Check for feedback prompts
if (this.config.enableFeedback && success) {
await this.checkFeedbackPrompts('command_completion');
}
}
async trackError(error, command, severity = 'medium') {
if (!this.config.analytics?.trackErrors || !this.currentSession)
return;
const errorMetric = {
errorId: this.generateErrorId(),
timestamp: new Date(),
type: this.classifyError(error),
command,
message: error.message,
stack: error.stack,
severity,
recoverable: this.isRecoverableError(error),
context: {
workingDirectory: process.cwd(),
userInput: command || '',
systemState: await this.collectSystemState(),
retryAttempts: 0
}
};
this.currentSession.errors.push(errorMetric);
this.emit('error:tracked', errorMetric);
// Check for feedback prompts
if (this.config.enableFeedback) {
await this.checkFeedbackPrompts('error_occurrence');
}
}
async collectFeedback(responses, trigger) {
if (!this.config.enableFeedback || !this.currentSession)
return;
const feedback = {
feedbackId: this.generateFeedbackId(),
sessionId: this.currentSession.sessionId,
userId: this.userId,
timestamp: new Date(),
trigger,
responses,
context: {
commandsExecuted: this.currentSession.commands.length,
errorsEncountered: this.currentSession.errors.length,
sessionDuration: Date.now() - this.currentSession.startTime.getTime(),
userSegment: await this.getUserSegment(),
featureUsed: this.getUsedFeatures()
},
sentiment: this.analyzeSentiment(responses),
nps: this.extractNPS(responses),
satisfaction: this.extractSatisfaction(responses)
};
await this.saveFeedbackData(feedback);
this.emit('feedback:collected', feedback);
}
async generateAnalytics(startDate, endDate) {
const sessions = await this.loadSessionData(startDate, endDate);
const feedback = await this.loadFeedbackData(startDate, endDate);
return {
timeframe: { start: startDate, end: endDate },
userMetrics: this.analyzeUserMetrics(sessions),
commandMetrics: this.analyzeCommandMetrics(sessions),
errorMetrics: this.analyzeErrorMetrics(sessions),
performanceMetrics: this.analyzePerformanceMetrics(sessions),
feedbackMetrics: this.analyzeFeedbackMetrics(feedback),
usabilityMetrics: this.analyzeUsabilityMetrics(sessions, feedback),
retentionMetrics: this.analyzeRetentionMetrics(sessions),
satisfactionMetrics: this.analyzeSatisfactionMetrics(feedback)
};
}
async generateReport(startDate, endDate) {
const analytics = await this.generateAnalytics(startDate, endDate);
const insights = this.generateInsights(analytics);
const recommendations = this.generateRecommendations(analytics, insights);
const actionPlan = this.generateActionPlan(recommendations);
const report = {
summary: this.generateSummary(analytics),
analytics,
insights,
recommendations,
actionPlan,
timestamp: new Date()
};
// Save report
await this.saveReport(report);
return report;
}
createMockSession() {
return {
sessionId: 'mock',
userId: 'mock',
startTime: new Date(),
platform: os.platform(),
nodeVersion: process.version,
cliVersion: '0.0.0',
commands: [],
errors: [],
performance: {
startupTime: 0,
memoryUsage: [],
cpuUsage: [],
commandLatencies: [],
averageResponseTime: 0,
peakMemoryUsage: 0,
totalCommands: 0,
errorRate: 0
},
userAgent: {
terminal: 'unknown',
shell: 'unknown',
terminalSize: { width: 80, height: 24 },
colorSupport: false,
interactiveMode: false
},
context: {
workspaceType: 'single_project',
projectSize: 'small',
packageManager: 'unknown',
gitRepository: false,
hasConfigFile: false,
workspaceCount: 0,
dependencyCount: 0
}
};
}
generateSessionId() {
return `sess_${Date.now()}_${Math.random().toString(36).substring(2)}`;
}
generateCommandId() {
return `cmd_${Date.now()}_${Math.random().toString(36).substring(2)}`;
}
generateErrorId() {
return `err_${Date.now()}_${Math.random().toString(36).substring(2)}`;
}
generateFeedbackId() {
return `feedback_${Date.now()}_${Math.random().toString(36).substring(2)}`;
}
async getCLIVersion() {
try {
const packageJson = await fs.readJson(path.join(__dirname, '../../package.json'));
return packageJson.version || '0.0.0';
}
catch {
return '0.0.0';
}
}
async collectUserAgent() {
return {
terminal: process.env.TERM || 'unknown',
shell: process.env.SHELL || 'unknown',
terminalSize: {
width: process.stdout.columns || 80,
height: process.stdout.rows || 24
},
colorSupport: process.stdout.hasColors ? process.stdout.hasColors() : false,
interactiveMode: process.stdin.isTTY || false,
ciEnvironment: process.env.CI ? this.detectCIEnvironment() : undefined
};
}
detectCIEnvironment() {
if (process.env.GITHUB_ACTIONS)
return 'github';
if (process.env.GITLAB_CI)
return 'gitlab';
if (process.env.JENKINS_URL)
return 'jenkins';
if (process.env.CIRCLECI)
return 'circleci';
if (process.env.TRAVIS)
return 'travis';
return 'unknown';
}
async collectSessionContext() {
try {
const cwd = process.cwd();
let packageManager = 'unknown';
let dependencyCount = 0;
let hasConfigFile = false;
// Detect package manager
if (await fs.pathExists(path.join(cwd, 'yarn.lock'))) {
packageManager = 'yarn';
}
else if (await fs.pathExists(path.join(cwd, 'pnpm-lock.yaml'))) {
packageManager = 'pnpm';
}
else if (await fs.pathExists(path.join(cwd, 'package-lock.json'))) {
packageManager = 'npm';
}
else if (await fs.pathExists(path.join(cwd, 'bun.lockb'))) {
packageManager = 'bun';
}
// Check for package.json and count dependencies
const packageJsonPath = path.join(cwd, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
try {
const packageJson = await fs.readJson(packageJsonPath);
const deps = {
...packageJson.dependencies,
...packageJson.devDependencies
};
dependencyCount = Object.keys(deps).length;
}
catch {
// Ignore errors
}
}
// Check for Re-Shell config
hasConfigFile = await fs.pathExists(path.join(cwd, '.re-shell.config.yaml')) ||
await fs.pathExists(path.join(cwd, 're-shell.config.js'));
// Detect workspace type
let workspaceType = 'empty';
let workspaceCount = 0;
if (await fs.pathExists(path.join(cwd, 'lerna.json')) ||
await fs.pathExists(path.join(cwd, 'nx.json')) ||
await fs.pathExists(path.join(cwd, 'rush.json'))) {
workspaceType = 'monorepo';
// Count workspaces (simplified)
try {
const files = await fs.readdir(cwd);
workspaceCount = files.filter(f => f.startsWith('packages') || f.startsWith('apps')).length;
}
catch {
workspaceCount = 1;
}
}
else if (await fs.pathExists(packageJsonPath)) {
workspaceType = 'single_project';
workspaceCount = 1;
}
// Determine project size
let projectSize = 'small';
if (dependencyCount > 50 || workspaceCount > 5) {
projectSize = 'large';
}
else if (dependencyCount > 20 || workspaceCount > 2) {
projectSize = 'medium';
}
return {
workspaceType,
projectSize,
packageManager: packageManager,
gitRepository: await fs.pathExists(path.join(cwd, '.git')),
hasConfigFile,
workspaceCount,
dependencyCount
};
}
catch {
return {
workspaceType: 'empty',
projectSize: 'small',
packageManager: 'unknown',
gitRepository: false,
hasConfigFile: false,
workspaceCount: 0,
dependencyCount: 0
};
}
}
startPerformanceMonitoring() {
if (!this.currentSession || !this.config.analytics?.trackPerformance)
return;
const interval = setInterval(() => {
if (!this.currentSession) {
clearInterval(interval);
return;
}
const memUsage = process.memoryUsage();
const memMetric = {
timestamp: new Date(),
heapUsed: memUsage.heapUsed,
heapTotal: memUsage.heapTotal,
external: memUsage.external,
rss: memUsage.rss
};
this.currentSession.performance.memoryUsage.push(memMetric);
if (memUsage.rss > this.currentSession.performance.peakMemoryUsage) {
this.currentSession.performance.peakMemoryUsage = memUsage.rss;
}
}, this.config.collectionInterval);
}
calculateSessionPerformance() {
if (!this.currentSession)
return;
const perf = this.currentSession.performance;
// Calculate average response time
if (perf.commandLatencies.length > 0) {
perf.averageResponseTime = perf.commandLatencies.reduce((a, b) => a + b, 0) / perf.commandLatencies.length;
}
// Calculate error rate
const totalCommands = this.currentSession.commands.length;
const errorCommands = this.currentSession.commands.filter(c => !c.success).length;
perf.errorRate = totalCommands > 0 ? errorCommands / totalCommands : 0;
}
classifyError(error) {
const message = error.message.toLowerCase();
if (message.includes('network') || message.includes('connection') || message.includes('timeout')) {
return 'network_error';
}
if (message.includes('invalid') || message.includes('validation') || message.includes('required')) {
return 'validation_error';
}
if (message.includes('command') || message.includes('not found') || message.includes('permission')) {
return 'command_error';
}
return 'system_error';
}
isRecoverableError(error) {
const message = error.message.toLowerCase();
// Non-recoverable errors
if (message.includes('fatal') || message.includes('corrupt') || message.includes('permission denied')) {
return false;
}
// Recoverable errors
if (message.includes('network') || message.includes('timeout') || message.includes('not found')) {
return true;
}
return true; // Default to recoverable
}
async collectSystemState() {
return {
availableMemory: os.freemem(),
diskSpace: 0, // Would require additional system calls
networkConnectivity: true, // Would require network check
permissions: [] // Would require permission check
};
}
async checkFeedbackPrompts(trigger) {
if (!this.config.feedbackPrompts)
return;
for (const prompt of this.config.feedbackPrompts) {
if (prompt.trigger === trigger) {
const shouldPrompt = await this.shouldShowFeedbackPrompt(prompt);
if (shouldPrompt) {
this.emit('feedback:prompt', prompt);
break; // Only show one prompt at a time
}
}
}
}
async shouldShowFeedbackPrompt(prompt) {
if (!this.currentSession)
return false;
// Check frequency
if (prompt.trigger === 'command_completion') {
return this.currentSession.commands.length % prompt.frequency === 0;
}
if (prompt.trigger === 'error_occurrence') {
return this.currentSession.errors.length >= prompt.frequency;
}
// Check conditions
if (prompt.conditions) {
for (const condition of prompt.conditions) {
if (!this.evaluateCondition(condition)) {
return false;
}
}
}
return true;
}
evaluateCondition(condition) {
if (!this.currentSession)
return false;
let value;
switch (condition.type) {
case 'command_count':
value = this.currentSession.commands.length;
break;
case 'error_count':
value = this.currentSession.errors.length;
break;
case 'session_duration':
value = Date.now() - this.currentSession.startTime.getTime();
break;
default:
return false;
}
switch (condition.operator) {
case 'gt': return value > condition.value;
case 'lt': return value < condition.value;
case 'eq': return value === condition.value;
case 'gte': return value >= condition.value;
case 'lte': return value <= condition.value;
default: return false;
}
}
async getUserSegment() {
if (!this.currentSession)
return 'unknown';
// Simple segmentation based on usage patterns
const commandCount = this.currentSession.commands.length;
const errorRate = this.currentSession.performance.errorRate;
if (commandCount > 50 && errorRate < 0.1) {
return 'power_user';
}
else if (commandCount > 20) {
return 'regular_user';
}
else if (errorRate > 0.3) {
return 'struggling_user';
}
else {
return 'new_user';
}
}
getUsedFeatures() {
if (!this.currentSession)
return [];
const features = new Set();
for (const command of this.currentSession.commands) {
features.add(command.command);
// Add feature flags
Object.keys(command.flags).forEach(flag => {
features.add(`flag:${flag}`);
});
}
return Array.from(features);
}
analyzeSentiment(responses) {
let totalSentiment = 0;
let sentimentCount = 0;
for (const response of responses) {
if (response.type === 'rating' && typeof response.value === 'number') {
totalSentiment += response.value;
sentimentCount++;
}
}
if (sentimentCount === 0)
return 'neutral';
const averageSentiment = totalSentiment / sentimentCount;
if (averageSentiment >= 4)
return 'positive';
if (averageSentiment <= 2)
return 'negative';
return 'neutral';
}
extractNPS(responses) {
const npsResponse = responses.find(r => r.questionId === 'nps');
return npsResponse && typeof npsResponse.value === 'number' ? npsResponse.value : undefined;
}
extractSatisfaction(responses) {
const satisfactionResponse = responses.find(r => r.questionId === 'satisfaction');
return satisfactionResponse && typeof satisfactionResponse.value === 'number' ? satisfactionResponse.value : undefined;
}
async saveSessionData(session) {
if (!this.config.enableCollection)
return;
try {
const filename = `session_${session.sessionId}_${session.startTime.toISOString().split('T')[0]}.json`;
const filepath = path.join(this.storageLocation, 'sessions', filename);
await fs.ensureDir(path.dirname(filepath));
await fs.writeJson(filepath, session, { spaces: 2 });
}
catch (error) {
this.emit('error', error);
}
}
async saveFeedbackData(feedback) {
if (!this.config.enableCollection)
return;
try {
const filename = `feedback_${feedback.feedbackId}_${feedback.timestamp.toISOString().split('T')[0]}.json`;
const filepath = path.join(this.storageLocation, 'feedback', filename);
await fs.ensureDir(path.dirname(filepath));
await fs.writeJson(filepath, feedback, { spaces: 2 });
}
catch (error) {
this.emit('error', error);
}
}
async loadSessionData(startDate, endDate) {
try {
const sessionsDir = path.join(this.storageLocation, 'sessions');
if (!await fs.pathExists(sessionsDir))
return [];
const files = await fs.readdir(sessionsDir);
const sessions = [];
for (const file of files) {
try {
const session = await fs.readJson(path.join(sessionsDir, file));
const sessionDate = new Date(session.startTime);
if (sessionDate >= startDate && sessionDate <= endDate) {
sessions.push(session);
}
}
catch {
// Skip corrupted files
}
}
return sessions;
}
catch {
return [];
}
}
async loadFeedbackData(startDate, endDate) {
try {
const feedbackDir = path.join(this.storageLocation, 'feedback');
if (!await fs.pathExists(feedbackDir))
return [];
const files = await fs.readdir(feedbackDir);
const feedback = [];
for (const file of files) {
try {
const feedbackData = await fs.readJson(path.join(feedbackDir, file));
const feedbackDate = new Date(feedbackData.timestamp);
if (feedbackDate >= startDate && feedbackDate <= endDate) {
feedback.push(feedbackData);
}
}
catch {
// Skip corrupted files
}
}
return feedback;
}
catch {
return [];
}
}
analyzeUserMetrics(sessions) {
const uniqueUsers = new Set(sessions.map(s => s.userId)).size;
const platforms = new Map();
sessions.forEach(session => {
platforms.set(session.platform, (platforms.get(session.platform) || 0) + 1);
});
return {
totalUsers: uniqueUsers,
activeUsers: uniqueUsers, // Simplified
newUsers: Math.floor(uniqueUsers * 0.3), // Mock estimate
returningUsers: uniqueUsers - Math.floor(uniqueUsers * 0.3),
userSegments: [
{
segment: 'power_user',
count: Math.floor(uniqueUsers * 0.2),
characteristics: { avgCommands: 50, avgErrors: 2 },
behavior: {
averageSessionDuration: 600000,
commandsPerSession: 50,
errorRate: 0.04,
featureAdoption: {},
satisfaction: 4.5
}
}
],
geographicDistribution: [
{ region: 'US', count: Math.floor(uniqueUsers * 0.6), averagePerformance: 100, topCommands: ['init', 'build'] }
],
platformDistribution: Array.from(platforms.entries()).map(([platform, count]) => ({
platform,
count,
performance: { averageStartupTime: 1000, averageCommandTime: 500, memoryEfficiency: 80 },
errorRate: 0.05
}))
};
}
analyzeCommandMetrics(sessions) {
const allCommands = sessions.flatMap(s => s.commands);
const commandCounts = new Map();
allCommands.forEach(cmd => {
commandCounts.set(cmd.command, (commandCounts.get(cmd.command) || 0) + 1);
});
const totalCommands = allCommands.length;
const successfulCommands = allCommands.filter(c => c.success).length;
return {
totalCommands,
uniqueCommands: commandCounts.size,
commandFrequency: Array.from(commandCounts.entries())
.map(([command, count]) => ({
command,
count,
percentage: (count / totalCommands) * 100,
trend: 'stable'
}))
.sort((a, b) => b.count - a.count),
commandSuccess: {
overallSuccessRate: (successfulCommands / totalCommands) * 100,
commandSuccessRates: {},
failureReasons: [],
retryPatterns: []
},
commandPerformance: {
averageExecutionTime: {},
performanceTrends: [],
performanceIssues: []
},
commandSequences: [],
abandonmentPoints: []
};
}
analyzeErrorMetrics(sessions) {
const allErrors = sessions.flatMap(s => s.errors);
const errorTypes = new Map();
allErrors.forEach(error => {
errorTypes.set(error.type, (errorTypes.get(error.type) || 0) + 1);
});
return {
totalErrors: allErrors.length,
errorRate: sessions.length > 0 ? allErrors.length / sessions.length : 0,
errorTypes: Array.from(errorTypes.entries()).map(([type, count]) => ({
type,
count,
percentage: (count / allErrors.length) * 100,
averageResolutionTime: 300000, // 5 minutes mock
userImpact: 'medium'
})),
errorTrends: [],
errorImpact: {
blockedUsers: 0,
lostSessions: 0,
supportTickets: 0,
productivityLoss: 0
},
resolutionMetrics: {
averageResolutionTime: 300000,
selfServiceRate: 80,
escalationRate: 20,
resolutionMethods: []
}
};
}
analyzePerformanceMetrics(sessions) {
const performances = sessions.map(s => s.performance);
const avgStartupTime = performances.reduce((sum, p) => sum + p.startupTime, 0) / performances.length;
const avgCommandTime = performances.reduce((sum, p) => sum + p.averageResponseTime, 0) / performances.length;
return {
averageStartupTime: avgStartupTime,
averageCommandTime: avgCommandTime,
memoryEfficiency: 85, // Mock
performanceScores: [
{ metric: 'startup', score: 85, benchmark: 90, trend: 'stable' },
{ metric: 'command', score: 80, benchmark: 85, trend: 'improving' }
],
bottlenecks: [],
improvements: []
};
}
analyzeFeedbackMetrics(feedback) {
if (feedback.length === 0) {
return {
responseRate: 0,
averageSatisfaction: 0,
npsScore: 0,
sentimentDistribution: { positive: 0, negative: 0, neutral: 0 },
feedbackCategories: [],
improvementSuggestions: []
};
}
const satisfactionScores = feedback
.map(f => f.satisfaction)
.filter(s => s !== undefined);
const npsScores = feedback
.map(f => f.nps)
.filter(n => n !== undefined);
const sentiments = feedback.map(f => f.sentiment);
const sentimentCounts = {
positive: sentiments.filter(s => s === 'positive').length,
negative: sentiments.filter(s => s === 'negative').length,
neutral: sentiments.filter(s => s === 'neutral').length
};
return {
responseRate: 75, // Mock response rate
averageSatisfaction: satisfactionScores.length > 0
? satisfactionScores.reduce((a, b) => a + b, 0) / satisfactionScores.length
: 0,
npsScore: npsScores.length > 0
? npsScores.reduce((a, b) => a + b, 0) / npsScores.length
: 0,
sentimentDistribution: {
positive: (sentimentCounts.positive / feedback.length) * 100,
negative: (sentimentCounts.negative / feedback.length) * 100,
neutral: (sentimentCounts.neutral / feedback.length) * 100
},
feedbackCategories: [],
improvementSuggestions: []
};
}
analyzeUsabilityMetrics(sessions, feedback) {
return {
easeOfUse: 4.2,
learnability: 4.0,
efficiency: 4.1,
memorability: 4.3,
errorPrevention: 3.8,
accessibility: {
screenReaderCompatibility: 60,
colorBlindnessSupport: 80,
keyboardNavigation: 90,
textScaling: 85,
contrastRatio: 95
},
taskCompletion: {
overallCompletionRate: 85,
taskCompletionRates: {},
averageTaskTime: {},
taskDifficulty: {}
}
};
}
analyzeRetentionMetrics(sessions) {
const uniqueUsers = new Set(sessions.map(s => s.userId)).size;
return {
dailyActiveUsers: Math.floor(uniqueUsers * 0.3),
weeklyActiveUsers: Math.floor(uniqueUsers * 0.6),
monthlyActiveUsers: uniqueUsers,
retentionRates: [
{ period: 'day1', rate: 85, trend: 'stable' },
{ period: 'day7', rate: 65, trend: 'improving' },
{ period: 'day30', rate: 45, trend: 'stable' }
],
churnAnalysis: {
churnRate: 15,
churnReasons: [
{ reason: 'complexity', frequency: 40, impact: 'high' },
{ reason: 'performance', frequency: 30, impact: 'medium' }
],
riskFactors: [],
preventionStrategies: []
},
cohortAnalysis: []
};
}
analyzeSatisfactionMetrics(feedback) {
const satisfactionScores = feedback
.map(f => f.satisfaction)
.filter(s => s !== undefined);
const avgSatisfaction = satisfactionScores.length > 0
? satisfactionScores.reduce((a, b) => a + b, 0) / satisfactionScores.length
: 0;
return {
overallSatisfaction: avgSatisfaction,
featureSatisfaction: {},
satisfactionTrends: [],
satisfactionDrivers: [],
detractors: []
};
}
generateInsights(analytics) {
return {
userBehavior: [
{
insight: 'Users prefer command completion over manual typing',
evidence: ['High usage of tab completion', 'Lower error rates with completion'],
impact: 'medium',
userSegments: ['all']
}
],
performanceInsights: [
{
area: 'startup',
finding: 'Startup time varies significantly across platforms',
impact: 'User satisfaction decreases with longer startup times',
recommendation: 'Optimize startup sequence for slower platforms'
}
],
usabilityInsights: [],
satisfactionInsights: [],
opportunityAreas: [
{
area: 'Error Handling',
opportunity: 'Improve error message clarity and actionability',
potentialImpact: 'High reduction in user frustration and support requests',
effort: 'medium',
priority: 'high'
}
]
};
}
generateRecommendations(analytics, insights) {
return [
{
id: 'improve-error-messages',
category: 'usability',
priority: 'high',
title: 'Improve Error Message Clarity',
description: 'Enhance error messages with clear explanations and actionable next steps',
rationale: 'Users frequently struggle with unclear error messages, leading to frustration',
expectedImpact: {
satisfaction: 15,
efficiency: 20,
errorReduction: 25,
retention: 10,
adoption: 5
},
implementation: {
effort: 'medium',
timeline: '2-3 weeks',
resources: ['UX writer', 'Developer'],
dependencies: ['Error taxonomy'],
risks: ['Increased message length'],
steps: [
{
step: 1,
description: 'Audit existing error messages',
deliverable: 'Error message inventory',
timeline: '1 week',
owner: 'UX Team'
},
{
step: 2,
description: 'Redesign error message templates',
deliverable: 'New error message guidelines',
timeline: '1 week',
owner: 'UX Writer'
}
]
},
metrics: ['Error resolution time', 'User satisfaction', 'Support ticket volume'],
successCriteria: [
'25% reduction in error-related support tickets',
'20% improvement in error resolution time',
'15% increase in user satisfaction scores'
]
}
];
}
generateActionPlan(recommendations) {
const critical = recommendations.filter(r => r.priority === 'critical');
const high = recommendations.filter(r => r.priority === 'high');
const medium = recommendations.filter(r => r.priority === 'medium');
return {
immediate: critical.map(this.recommendationToAction),
shortTerm: high.map(this.recommendationToAction),
longTerm: medium.map(this.recommendationToAction),
monitoring: {
metrics: ['User satisfaction', 'Error rate', 'Task completion rate'],
frequency: 'weekly',
alerts: [
{
metric: 'Error rate',
threshold: 10,
severity: 'high',
action: 'Investigate error spike'
}
],
reports: [
{
type: 'UX Dashboard',
frequency: 'weekly',
recipients: ['Product Team', 'Development Team'],
format: 'dashboard'
}
]
}
};
}
generateSummary(analytics) {
return {
timeframe: `${analytics.timeframe.start.toDateString()} - ${analytics.timeframe.end.toDateString()}`,
totalUsers: analytics.userMetrics.totalUsers,
satisfaction: analytics.satisfactionMetrics.overallSatisfaction,
npsScore: analytics.feedbackMetrics.npsScore,
errorRate: analytics.errorMetrics.errorRate,
keyMetrics: [
{
name: 'User Satisfaction',
value: analytics.satisfactionMetrics.overallSatisfaction,
change: 5,
trend: 'up',
status: 'good'
},
{
name: 'Error Rate',
value: analytics.errorMetrics.errorRate,
change: -2,
trend: 'down',
status: 'good'
}
],
highlights: [
'User satisfaction increased by 5%',
'Error rate decreased by 2%',
'Command completion rate improved to 85%'
],
concerns: [
'Startup time varies significantly across platforms',
'Some error messages remain unclear to users'
]
};
}
async saveReport(report) {
try {
const reportDir = path.join(this.storageLocation, 'reports');
await fs.ensureDir(reportDir);
const filename = `ux-report-${report.timestamp.toISOString().split('T')[0]}.json`;
const filepath = path.join(reportDir, filename);
await fs.writeJson(filepath, report, { spaces: 2 });
// Also save HTML version
const htmlReport = this.generateHtmlReport(report);
const htmlPath = path.join(reportDir, filename.replace('.json', '.html'));
await fs.writeFile(htmlPath, htmlReport);
this.emit('report:saved', { json: filepath, html: htmlPath });
}
catch (error) {
this.emit('error', error);
}
}
generateHtmlReport(report) {
return `<!DOCTYPE html>
<html>
<head>
<title>UX Metrics Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }
.header { background: #4CAF50; color: white; padding: 20px; border-radius: 5px; }
.summary { background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0; }
.metric { display: inline-block; margin: 10px; padding: 10px; background: white; border-radius: 3px; }
.score { font-size: 1.5em; font-weight: bold; }
.good { color: #4CAF50; }
.warning { color: #FF9800; }
.critical { color: #F44336; }
.recommendation { margin: 10px 0; padding: 15px; border-left: 4px solid #2196F3; background: #f8f9fa; }
.high { border-left-color: #F44336; }
.medium { border-left-color: #FF9800; }
.low { border-left-color: #4CAF50; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
th { background-color: #f2f2f2; }
.insight { margin: 10px 0; padding: 10px; background: #e3f2fd; border-radius: 3px; }
</style>
</head>
<body>
<div class="header">
<h1>📊 User Experience Report</h1>
<p>Period: ${report.summary.timeframe}</p>
<p>Overall Satisfaction: <span class="score">${report.summary.satisfaction.toFixed(1)}/5</span></p>
</div>
<div class="summary">
<h2>Executive Summary</h2>
<div class="metric">
<strong>Total Users:</strong> ${report.summary.totalUsers}
</div>
<div class="metric">
<strong>NPS Score:</strong> <span class="${report.summary.npsScore > 50 ? 'good' : report.summary.npsScore > 0 ? 'warning' : 'critical'}">${report.summary.npsScore.toFixed(0)}</span>
</div>
<div class="metric">
<strong>Error Rate:</strong> <span class="${report.summary.errorRate < 5 ? 'good' : report.summary.errorRate < 10 ? 'warning' : 'critical'}">${report.summary.errorRate.toFixed(1)}%</span>
</div>
</div>
<h2>Key Metrics</h2>
<table>
<tr>
<th>Metric</th>
<th>Value</th>
<th>Change</th>
<th>Status</th>
</tr>
${report.summary.keyMetrics.map(metric => `
<tr>
<td>${metric.name}</td>
<td>${metric.value.toFixed(1)}</td>
<td class="${metric.trend === 'up' ? 'good' : metric.trend === 'down' ? 'critical' : ''}">${metric.change > 0 ? '+' : ''}${metric.change.toFixed(1)}%</td>
<td><span class="${metric.status}">${metric.status.toUpperCase()}</span></td>
</tr>
`).join('')}
</table>
<h2>Highlights</h2>
<ul>
${report.summary.highlights.map(highlight => `<li>${highlight}</li>`).join('')}
</ul>
<h2>Areas of Concern</h2>
<ul>
${report.summary.concerns.map(concern => `<li>${concern}</li>`).join('')}
</ul>
<h2>Top Recommendations</h2>
${report.recommendations.slice(0, 5).map(rec => `
<div class="recommendation ${rec.priority}">
<h4>${rec.title} (${rec.priority} priority)</h4>
<p>${rec.description}</p>
<p><strong>Expected Impact:</strong> ${rec.expectedImpact.satisfaction}% satisfaction improvement</p>
<p><strong>Timeline:</strong> ${rec.implementation.timeline}</p>
<p><strong>Effort:</strong> ${rec.implementation.effort}</p>
</div>
`).join('')}
<h2>Key Insights</h2>
${report.insights.opportunityAreas.map(area => `
<div class="insight">
<h4>${area.area}</h4>
<p><strong>Opportunity:</strong> ${area.opportunity}</p>
<p><strong>Impact:</strong> ${area.potentialImpact}</p>
<p><strong>Priority:</strong> ${area.priority} | <strong>Effort:</strong> ${area.effort}</p>
</div>
`).join('')}
<footer>
<p>Generated on ${report.timestamp.toISOString()}</p>
<p>Report covers ${report.analytics.userMetrics.totalUsers} users and ${report.analytics.commandMetrics.totalCommands} commands</p>
</footer>
</body>
</html>`;
}
}
exports.UXMetricsCollector = UXMetricsCollector;
// Export utility functions
async function collectUXMetrics(config) {
const collector = new UXMetricsCollector(config);
return collector;
}
async function generateUXReport(startDate, endDate, config) {
const collector = new UXMetricsCollector(config);
return collector.generateReport(startDate, endDate);
}