UNPKG

@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

667 lines (666 loc) 26.4 kB
"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.TemplateAnalytics = exports.InsightType = exports.UsageEventType = void 0; exports.createTemplateAnalytics = createTemplateAnalytics; exports.getGlobalTemplateAnalytics = getGlobalTemplateAnalytics; exports.setGlobalTemplateAnalytics = setGlobalTemplateAnalytics; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); var UsageEventType; (function (UsageEventType) { UsageEventType["INSTALLED"] = "installed"; UsageEventType["CREATED"] = "created"; UsageEventType["UPDATED"] = "updated"; UsageEventType["REMOVED"] = "removed"; UsageEventType["VALIDATED"] = "validated"; UsageEventType["PUBLISHED"] = "published"; UsageEventType["DOWNLOADED"] = "downloaded"; UsageEventType["FAILED"] = "failed"; UsageEventType["CUSTOMIZED"] = "customized"; })(UsageEventType || (exports.UsageEventType = UsageEventType = {})); var InsightType; (function (InsightType) { InsightType["PERFORMANCE"] = "performance"; InsightType["RELIABILITY"] = "reliability"; InsightType["ADOPTION"] = "adoption"; InsightType["ERRORS"] = "errors"; InsightType["TRENDS"] = "trends"; InsightType["OPPORTUNITIES"] = "opportunities"; })(InsightType || (exports.InsightType = InsightType = {})); class TemplateAnalytics extends events_1.EventEmitter { constructor(dataDir, config = {}) { super(); this.dataDir = dataDir; this.config = config; this.events = []; this.metricsCache = new Map(); this.defaultConfig = { enabled: true, anonymize: true, storageLimit: 10 * 1024 * 1024, // 10MB retentionDays: 90, syncInterval: 60 * 60 * 1000, // 1 hour includeSystemInfo: true, trackVariables: true }; this.config = { ...this.defaultConfig, ...config }; this.storagePath = path.join(dataDir, 'analytics'); this.sessionId = this.generateSessionId(); this.initialize(); } async initialize() { if (!this.config.enabled) return; await fs.ensureDir(this.storagePath); await this.loadEvents(); await this.cleanupOldEvents(); if (this.config.remoteEndpoint && this.config.syncInterval) { this.startSyncing(); } } async loadEvents() { try { const files = await fs.readdir(this.storagePath); const eventFiles = files.filter(f => f.endsWith('.json')); for (const file of eventFiles) { const filePath = path.join(this.storagePath, file); const data = await fs.readJson(filePath); if (Array.isArray(data)) { this.events.push(...data.map(e => ({ ...e, timestamp: new Date(e.timestamp) }))); } } // Sort by timestamp this.events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); } catch (error) { this.emit('error', { type: 'load_events', error }); } } async cleanupOldEvents() { if (!this.config.retentionDays) return; const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays); const oldCount = this.events.length; this.events = this.events.filter(e => e.timestamp > cutoffDate); if (oldCount !== this.events.length) { await this.persistEvents(); this.emit('cleanup:complete', { removed: oldCount - this.events.length }); } } startSyncing() { this.syncInterval = setInterval(() => { this.syncToRemote().catch(error => { this.emit('error', { type: 'sync', error }); }); }, this.config.syncInterval); } async trackUsage(template, eventType, context = {}, metadata) { if (!this.config.enabled) return; const event = { id: this.generateEventId(), templateId: template.id, version: template.version, eventType, timestamp: new Date(), success: true, context: { platform: process.platform, nodeVersion: process.version, cliVersion: await this.getCliVersion(), ...context }, metadata }; // Anonymize if needed if (this.config.anonymize) { event.context = this.anonymizeContext(event.context); } this.events.push(event); this.emit('event:tracked', event); // Persist periodically if (this.events.length % 100 === 0) { await this.persistEvents(); } // Check storage limit await this.checkStorageLimit(); } async trackError(template, error, context = {}) { await this.trackUsage(template, UsageEventType.FAILED, context, { error: error.message, stack: this.config.anonymize ? undefined : error.stack }); } async getMetrics(templateId) { // Check cache const cached = this.metricsCache.get(templateId); if (cached) { return cached; } const events = this.events.filter(e => e.templateId === templateId); if (events.length === 0) { return null; } const metrics = this.calculateMetrics(templateId, events); this.metricsCache.set(templateId, metrics); return metrics; } calculateMetrics(templateId, events) { const totalUsage = events.length; const successCount = events.filter(e => e.success).length; const durations = events.filter(e => e.duration).map(e => e.duration); // Variable analysis const variableUsage = new Map(); events.forEach(event => { if (event.context.variables && this.config.trackVariables) { Object.entries(event.context.variables).forEach(([key, value]) => { if (!variableUsage.has(key)) { variableUsage.set(key, new Map()); } const valueStr = JSON.stringify(value); const counts = variableUsage.get(key); counts.set(valueStr, (counts.get(valueStr) || 0) + 1); }); } }); const popularVariables = Array.from(variableUsage.entries()).map(([name, values]) => ({ name, values: Array.from(values.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([v]) => JSON.parse(v)), count: Array.from(values.values()).reduce((a, b) => a + b, 0) })); // Platform distribution const platformDistribution = {}; events.forEach(e => { platformDistribution[e.context.platform] = (platformDistribution[e.context.platform] || 0) + 1; }); // Version distribution const versionDistribution = {}; events.forEach(e => { versionDistribution[e.version] = (versionDistribution[e.version] || 0) + 1; }); // Time series data (last 30 days) const timeSeriesData = this.generateTimeSeriesData(events, 30); // User segments const userSegments = this.analyzeUserSegments(events); return { templateId, totalUsage, successRate: totalUsage > 0 ? (successCount / totalUsage) * 100 : 0, averageDuration: durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0, errorRate: totalUsage > 0 ? ((totalUsage - successCount) / totalUsage) * 100 : 0, popularVariables, platformDistribution, versionDistribution, timeSeriesData, userSegments }; } generateTimeSeriesData(events, days) { const data = []; const now = new Date(); for (let i = days - 1; i >= 0; i--) { const date = new Date(now); date.setDate(date.getDate() - i); date.setHours(0, 0, 0, 0); const dayEnd = new Date(date); dayEnd.setDate(dayEnd.getDate() + 1); const dayEvents = events.filter(e => e.timestamp >= date && e.timestamp < dayEnd); const uniqueUsers = new Set(dayEvents.map(e => e.context.projectName || 'unknown')); data.push({ date, usage: dayEvents.length, success: dayEvents.filter(e => e.success).length, errors: dayEvents.filter(e => !e.success).length, uniqueUsers: uniqueUsers.size }); } return data; } analyzeUserSegments(events) { const segments = []; const totalUsers = new Set(events.map(e => e.context.projectName || 'unknown')).size; // Platform segments const platformGroups = this.groupBy(events, e => e.context.platform); for (const [platform, platformEvents] of Object.entries(platformGroups)) { const users = new Set(platformEvents.map(e => e.context.projectName || 'unknown')); segments.push({ name: `${platform} Users`, description: `Users on ${platform} platform`, userCount: users.size, percentage: (users.size / totalUsers) * 100, characteristics: { platform } }); } // Activity segments const userActivity = new Map(); events.forEach(e => { const user = e.context.projectName || 'unknown'; userActivity.set(user, (userActivity.get(user) || 0) + 1); }); const powerUsers = Array.from(userActivity.entries()) .filter(([_, count]) => count > 10) .map(([user]) => user); if (powerUsers.length > 0) { segments.push({ name: 'Power Users', description: 'Users with >10 template operations', userCount: powerUsers.length, percentage: (powerUsers.length / totalUsers) * 100, characteristics: { minOperations: 10 } }); } return segments; } async generateReport(startDate, endDate, templateIds) { const start = startDate || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days const end = endDate || new Date(); const filteredEvents = this.events.filter(e => { if (e.timestamp < start || e.timestamp > end) return false; if (templateIds && !templateIds.includes(e.templateId)) return false; return true; }); const uniqueTemplates = new Set(filteredEvents.map(e => e.templateId)); const uniqueUsers = new Set(filteredEvents.map(e => e.context.projectName || 'unknown')); const successCount = filteredEvents.filter(e => e.success).length; const durations = filteredEvents.filter(e => e.duration).map(e => e.duration); // Top templates const templateUsage = this.groupBy(filteredEvents, e => e.templateId); const topTemplates = Object.entries(templateUsage) .map(([templateId, events]) => { const successRate = events.filter(e => e.success).length / events.length * 100; const trend = this.calculateTrend(templateId, start, end); return { templateId, usage: events.length, successRate, trend }; }) .sort((a, b) => b.usage - a.usage) .slice(0, 10); // Category breakdown const categoryBreakdown = {}; // Note: This would need access to template metadata // Error analysis const errorAnalysis = this.analyzeErrors(filteredEvents); // Calculate average duration const allDurations = filteredEvents .filter(e => e.duration !== undefined) .map(e => e.duration); const averageDuration = allDurations.length > 0 ? allDurations.reduce((a, b) => a + b, 0) / allDurations.length : 0; // Generate insights const insights = this.generateInsights(filteredEvents, { totalEvents: filteredEvents.length, uniqueTemplates: uniqueTemplates.size, uniqueUsers: uniqueUsers.size, successRate: filteredEvents.length > 0 ? successCount / filteredEvents.length * 100 : 0, averageDuration }); return { period: { start, end }, summary: { totalEvents: filteredEvents.length, uniqueTemplates: uniqueTemplates.size, uniqueUsers: uniqueUsers.size, successRate: filteredEvents.length > 0 ? successCount / filteredEvents.length * 100 : 0, averageDuration: durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0 }, topTemplates, categoryBreakdown, errorAnalysis, insights }; } analyzeErrors(events) { const errorGroups = new Map(); events.filter(e => !e.success && e.metadata?.error).forEach(event => { const error = event.metadata.error; if (!errorGroups.has(error)) { errorGroups.set(error, { count: 0, templates: new Set() }); } const group = errorGroups.get(error); group.count++; group.templates.add(event.templateId); }); return Array.from(errorGroups.entries()) .map(([error, data]) => ({ error, count: data.count, templates: Array.from(data.templates), suggestions: this.getErrorSuggestions(error) })) .sort((a, b) => b.count - a.count) .slice(0, 10); } getErrorSuggestions(error) { const suggestions = []; if (error.includes('permission')) { suggestions.push('Check file system permissions'); suggestions.push('Run with appropriate user privileges'); } if (error.includes('not found')) { suggestions.push('Verify template dependencies are installed'); suggestions.push('Check template file paths'); } if (error.includes('timeout')) { suggestions.push('Increase timeout settings'); suggestions.push('Check network connectivity'); } return suggestions; } generateInsights(events, summary) { const insights = []; // Performance insights if (summary.averageDuration > 5000) { insights.push({ type: InsightType.PERFORMANCE, severity: 'warning', title: 'Slow Template Processing', description: `Average template processing time is ${Math.round(summary.averageDuration / 1000)}s`, recommendations: [ 'Consider optimizing template file operations', 'Review hook execution times', 'Enable caching for repeated operations' ] }); } // Reliability insights if (summary.successRate < 90) { insights.push({ type: InsightType.RELIABILITY, severity: 'critical', title: 'Low Success Rate', description: `Only ${summary.successRate.toFixed(1)}% of template operations succeed`, recommendations: [ 'Review error logs for common issues', 'Improve template validation', 'Add better error handling' ] }); } // Adoption insights const dailyAverage = events.length / 30; if (dailyAverage < 1) { insights.push({ type: InsightType.ADOPTION, severity: 'info', title: 'Low Template Usage', description: 'Templates are being used less than once per day on average', recommendations: [ 'Improve template documentation', 'Promote templates to developers', 'Create more useful templates' ] }); } // Trend insights const recentEvents = events.filter(e => e.timestamp > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)); const oldEvents = events.filter(e => e.timestamp < new Date(Date.now() - 23 * 24 * 60 * 60 * 1000) && e.timestamp > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)); if (recentEvents.length > oldEvents.length * 1.5) { insights.push({ type: InsightType.TRENDS, severity: 'info', title: 'Growing Template Usage', description: 'Template usage has increased by 50% in the last week', data: { recent: recentEvents.length, old: oldEvents.length } }); } return insights; } calculateTrend(templateId, startDate, endDate) { const midpoint = new Date((startDate.getTime() + endDate.getTime()) / 2); const firstHalf = this.events.filter(e => e.templateId === templateId && e.timestamp >= startDate && e.timestamp < midpoint).length; const secondHalf = this.events.filter(e => e.templateId === templateId && e.timestamp >= midpoint && e.timestamp <= endDate).length; if (secondHalf > firstHalf * 1.2) return 'up'; if (secondHalf < firstHalf * 0.8) return 'down'; return 'stable'; } groupBy(array, keyFn) { return array.reduce((groups, item) => { const key = keyFn(item); if (!groups[key]) { groups[key] = []; } groups[key].push(item); return groups; }, {}); } anonymizeContext(context) { const anonymized = { ...context }; // Remove sensitive paths if (anonymized.projectPath) { anonymized.projectPath = this.anonymizePath(anonymized.projectPath); } // Remove project name if it might be sensitive if (anonymized.projectName) { anonymized.projectName = this.hashString(anonymized.projectName); } // Anonymize variable values that might be sensitive if (anonymized.variables && this.config.trackVariables) { const safeVariables = {}; for (const [key, value] of Object.entries(anonymized.variables)) { if (this.isSensitiveVariable(key)) { safeVariables[key] = '<redacted>'; } else { safeVariables[key] = value; } } anonymized.variables = safeVariables; } return anonymized; } anonymizePath(filePath) { const home = require('os').homedir(); return filePath.replace(home, '~').replace(/\/Users\/[^\/]+/, '/Users/<user>'); } hashString(str) { const crypto = require('crypto'); return crypto.createHash('sha256').update(str).digest('hex').substring(0, 8); } isSensitiveVariable(name) { const sensitive = ['password', 'token', 'key', 'secret', 'credential']; return sensitive.some(s => name.toLowerCase().includes(s)); } async persistEvents() { try { // Group events by date const eventsByDate = this.groupBy(this.events, e => e.timestamp.toISOString().split('T')[0]); for (const [date, events] of Object.entries(eventsByDate)) { const filePath = path.join(this.storagePath, `events-${date}.json`); await fs.writeJson(filePath, events, { spaces: 2 }); } } catch (error) { this.emit('error', { type: 'persist', error }); } } async checkStorageLimit() { if (!this.config.storageLimit) return; const files = await fs.readdir(this.storagePath); let totalSize = 0; for (const file of files) { const stats = await fs.stat(path.join(this.storagePath, file)); totalSize += stats.size; } if (totalSize > this.config.storageLimit) { // Remove oldest files const sortedFiles = files.sort(); while (totalSize > this.config.storageLimit * 0.8 && sortedFiles.length > 0) { const fileToRemove = sortedFiles.shift(); const filePath = path.join(this.storagePath, fileToRemove); const stats = await fs.stat(filePath); totalSize -= stats.size; await fs.remove(filePath); } // Reload events this.events = []; await this.loadEvents(); } } async syncToRemote() { if (!this.config.remoteEndpoint || this.events.length === 0) return; try { // Get unsyced events (last 1000) const eventsToSync = this.events.slice(-1000); const response = await fetch(this.config.remoteEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: this.sessionId, events: eventsToSync }) }); if (response.ok) { this.emit('sync:complete', { count: eventsToSync.length }); } } catch (error) { this.emit('sync:error', error); } } generateEventId() { return `evt_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`; } generateSessionId() { return `session_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`; } async getCliVersion() { try { const packagePath = path.join(__dirname, '..', '..', 'package.json'); const pkg = await fs.readJson(packagePath); return pkg.version || 'unknown'; } catch { return 'unknown'; } } // Public methods isEnabled() { return this.config.enabled || false; } setEnabled(enabled) { this.config.enabled = enabled; if (!enabled && this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = undefined; } } async exportData(format = 'json') { if (format === 'json') { return JSON.stringify(this.events, null, 2); } // CSV export const headers = [ 'id', 'templateId', 'version', 'eventType', 'timestamp', 'success', 'duration', 'platform', 'nodeVersion', 'cliVersion' ]; const rows = [headers.join(',')]; for (const event of this.events) { const row = [ event.id, event.templateId, event.version, event.eventType, event.timestamp.toISOString(), event.success.toString(), event.duration || '', event.context.platform, event.context.nodeVersion, event.context.cliVersion ]; rows.push(row.map(v => `"${v}"`).join(',')); } return rows.join('\n'); } async clearData() { this.events = []; this.metricsCache.clear(); const files = await fs.readdir(this.storagePath); for (const file of files) { await fs.remove(path.join(this.storagePath, file)); } this.emit('data:cleared'); } stop() { if (this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = undefined; } this.persistEvents().catch(() => { }); } } exports.TemplateAnalytics = TemplateAnalytics; // Global analytics instance let globalAnalytics = null; function createTemplateAnalytics(dataDir, config) { return new TemplateAnalytics(dataDir, config); } function getGlobalTemplateAnalytics() { if (!globalAnalytics) { const dataDir = path.join(process.cwd(), '.re-shell', 'analytics'); globalAnalytics = new TemplateAnalytics(dataDir); } return globalAnalytics; } function setGlobalTemplateAnalytics(analytics) { globalAnalytics = analytics; }