UNPKG

recoder-analytics

Version:

Comprehensive analytics and monitoring for the Recoder.xyz ecosystem

464 lines 16.9 kB
"use strict"; /** * Usage Analytics System for Recoder.xyz Ecosystem * * Privacy-focused analytics with user consent and data protection */ 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.createExtensionAnalytics = exports.createWebAnalytics = exports.createCLIAnalytics = exports.UsageAnalytics = void 0; const events_1 = require("events"); const crypto = __importStar(require("crypto")); class UsageAnalytics extends events_1.EventEmitter { constructor(platform) { super(); this.events = new Map(); this.isEnabled = false; this.platform = platform; this.sessionId = this.generateSessionId(); this.privacySettings = this.getDefaultPrivacySettings(); this.initializeAnalytics(); } static getInstance(platform) { if (!UsageAnalytics.instance) { UsageAnalytics.instance = new UsageAnalytics(platform); } return UsageAnalytics.instance; } /** * Initialize analytics with user consent */ async initializeAnalytics() { // Load privacy settings from user configuration this.privacySettings = await this.loadPrivacySettings(); this.isEnabled = this.privacySettings.analyticsEnabled; if (this.isEnabled) { this.startAnalytics(); } } /** * Start analytics collection */ startAnalytics() { this.trackEvent('system', 'analytics_started', { platform: this.platform, version: this.getVersion() }); // Track session start this.trackEvent('session', 'session_started', { sessionId: this.sessionId, timestamp: Date.now() }); console.log('📈 Usage analytics started (privacy-compliant)'); } /** * Set user ID (hashed for privacy) */ setUserId(userId) { if (!this.isEnabled) return; // Hash user ID for privacy this.userId = this.hashUserId(userId); } /** * Track analytics event */ trackEvent(eventType, eventName, properties = {}) { if (!this.isEnabled) return; const event = { id: this.generateEventId(), timestamp: Date.now(), platform: this.platform, userId: this.userId, sessionId: this.sessionId, eventType, eventName, properties: this.sanitizeProperties(properties), metadata: this.getMetadata() }; // Store event const key = `${eventType}:${eventName}`; if (!this.events.has(key)) { this.events.set(key, []); } this.events.get(key).push(event); // Emit event for real-time processing this.emit('analytics:event', event); // Send to analytics service (if enabled) this.sendToAnalyticsService(event); } /** * Track code generation */ trackCodeGeneration(provider, language, tokens, success) { this.trackEvent('code_generation', 'generated', { provider, language, tokens, success, responseTime: Date.now() // Would be actual response time }); } /** * Track command usage */ trackCommandUsage(command, args, success) { this.trackEvent('command', 'executed', { command, argsCount: args.length, success, platform: this.platform }); } /** * Track error occurrence */ trackError(error, context = {}) { if (!this.privacySettings.errorReportingEnabled) return; this.trackEvent('error', 'occurred', { errorType: error.constructor.name, errorMessage: this.sanitizeErrorMessage(error.message), context: this.sanitizeProperties(context), stack: this.sanitizeStackTrace(error.stack) }); } /** * Track performance metrics */ trackPerformance(metric, value, unit) { if (!this.privacySettings.performanceMetricsEnabled) return; this.trackEvent('performance', metric, { value, unit, timestamp: Date.now() }); } /** * Track feature usage */ trackFeatureUsage(feature, action, metadata = {}) { if (!this.privacySettings.featureUsageEnabled) return; this.trackEvent('feature', `${feature}_${action}`, { feature, action, ...this.sanitizeProperties(metadata) }); } /** * Get usage metrics */ getUsageMetrics() { const now = Date.now(); const dayMs = 24 * 60 * 60 * 1000; const weekMs = 7 * dayMs; const monthMs = 30 * dayMs; const allEvents = Array.from(this.events.values()).flat(); return { daily: this.calculateDailyMetrics(allEvents, now - dayMs, now), weekly: this.calculateWeeklyMetrics(allEvents, now - weekMs, now), monthly: this.calculateMonthlyMetrics(allEvents, now - monthMs, now), features: this.calculateFeatureMetrics(allEvents) }; } /** * Update privacy settings */ updatePrivacySettings(settings) { this.privacySettings = { ...this.privacySettings, ...settings }; this.isEnabled = this.privacySettings.analyticsEnabled; // Save settings this.savePrivacySettings(this.privacySettings); if (!this.isEnabled) { this.trackEvent('system', 'analytics_disabled'); this.clearStoredData(); } else { this.startAnalytics(); } this.emit('privacy:updated', this.privacySettings); } /** * Get current privacy settings */ getPrivacySettings() { return { ...this.privacySettings }; } /** * Export user data (GDPR compliance) */ exportUserData() { if (!this.userId) { return { message: 'No user data available' }; } const userEvents = Array.from(this.events.values()) .flat() .filter(event => event.userId === this.userId); return { userId: this.userId, // Already hashed platform: this.platform, sessionCount: this.getUniqueSessionCount(userEvents), totalEvents: userEvents.length, events: userEvents, privacySettings: this.privacySettings, dataCollectionPeriod: { start: Math.min(...userEvents.map(e => e.timestamp)), end: Math.max(...userEvents.map(e => e.timestamp)) } }; } /** * Delete user data (GDPR compliance) */ deleteUserData() { if (!this.userId) return; // Remove user events for (const [key, events] of this.events.entries()) { const filteredEvents = events.filter(event => event.userId !== this.userId); this.events.set(key, filteredEvents); } // Clear user ID this.userId = undefined; this.emit('data:deleted'); } // Private methods generateSessionId() { return crypto.randomBytes(16).toString('hex'); } generateEventId() { return crypto.randomBytes(8).toString('hex'); } hashUserId(userId) { return crypto.createHash('sha256').update(userId).digest('hex').substring(0, 16); } sanitizeProperties(properties) { const sanitized = {}; for (const [key, value] of Object.entries(properties)) { // Remove sensitive data if (this.isSensitiveKey(key)) { continue; } // Sanitize string values if (typeof value === 'string') { sanitized[key] = this.sanitizeString(value); } else if (typeof value === 'number' || typeof value === 'boolean') { sanitized[key] = value; } else if (Array.isArray(value)) { sanitized[key] = value.length; // Only store array length } else if (typeof value === 'object' && value !== null) { sanitized[key] = '[Object]'; // Don't recurse into objects } } return sanitized; } isSensitiveKey(key) { const sensitiveKeys = [ 'password', 'token', 'key', 'secret', 'auth', 'credential', 'email', 'username', 'userId', 'apiKey', 'accessToken' ]; return sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive.toLowerCase())); } sanitizeString(str) { // Remove potential file paths, URLs, etc. return str .replace(/\/[^\s]+/g, '[PATH]') // Replace file paths .replace(/https?:\/\/[^\s]+/g, '[URL]') // Replace URLs .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL]') // Replace emails .substring(0, 200); // Limit length } sanitizeErrorMessage(message) { return this.sanitizeString(message); } sanitizeStackTrace(stack) { if (!stack) return ''; // Only keep the first few lines and sanitize paths return stack .split('\n') .slice(0, 5) .map(line => this.sanitizeString(line)) .join('\n'); } getMetadata() { return { version: this.getVersion(), os: process.platform, nodeVersion: process.version, // Browser/extension specific metadata would be added here }; } getVersion() { // Would get actual version from package.json return '1.0.0'; } async sendToAnalyticsService(event) { if (!this.privacySettings.shareAnonymousStats) return; try { // Send to analytics service (e.g., PostHog, Mixpanel, custom service) // Implementation would depend on chosen service // Example for custom service: // await fetch('https://analytics.recoder.xyz/events', { // method: 'POST', // headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify(event) // }); } catch (error) { // Silently fail - analytics should never break the app console.debug('Analytics service error:', error); } } calculateDailyMetrics(events, start, end) { const dayEvents = events.filter(e => e.timestamp >= start && e.timestamp <= end); return { users: this.getUniqueUserCount(dayEvents), sessions: this.getUniqueSessionCount(dayEvents), codeGenerations: dayEvents.filter(e => e.eventType === 'code_generation').length, tokensUsed: dayEvents .filter(e => e.eventType === 'code_generation') .reduce((sum, e) => sum + (e.properties.tokens || 0), 0), errors: dayEvents.filter(e => e.eventType === 'error').length }; } calculateWeeklyMetrics(events, start, end) { const weekEvents = events.filter(e => e.timestamp >= start && e.timestamp <= end); return { users: this.getUniqueUserCount(weekEvents), sessions: this.getUniqueSessionCount(weekEvents), codeGenerations: weekEvents.filter(e => e.eventType === 'code_generation').length, avgSessionDuration: this.calculateAverageSessionDuration(weekEvents), retentionRate: this.calculateRetentionRate(weekEvents, start, end) }; } calculateMonthlyMetrics(events, start, end) { const monthEvents = events.filter(e => e.timestamp >= start && e.timestamp <= end); return { users: this.getUniqueUserCount(monthEvents), newUsers: 0, // Would require historical data churnRate: 0, // Would require historical data totalGenerations: monthEvents.filter(e => e.eventType === 'code_generation').length, avgGenerationsPerUser: 0 // Calculated from above }; } calculateFeatureMetrics(events) { const providers = new Map(); const commands = new Map(); const languages = new Map(); events.forEach(event => { if (event.eventType === 'code_generation') { const provider = event.properties.provider; const language = event.properties.language; if (provider) providers.set(provider, (providers.get(provider) || 0) + 1); if (language) languages.set(language, (languages.get(language) || 0) + 1); } if (event.eventType === 'command') { const command = event.properties.command; if (command) commands.set(command, (commands.get(command) || 0) + 1); } }); return { mostUsedProviders: Array.from(providers.entries()) .map(([provider, usage]) => ({ provider, usage })) .sort((a, b) => b.usage - a.usage) .slice(0, 10), mostUsedCommands: Array.from(commands.entries()) .map(([command, usage]) => ({ command, usage })) .sort((a, b) => b.usage - a.usage) .slice(0, 10), popularLanguages: Array.from(languages.entries()) .map(([language, usage]) => ({ language, usage })) .sort((a, b) => b.usage - a.usage) .slice(0, 10), averageProjectSize: 0 // Would be calculated from project data }; } getUniqueUserCount(events) { const userIds = new Set(events.map(e => e.userId).filter(Boolean)); return userIds.size; } getUniqueSessionCount(events) { const sessionIds = new Set(events.map(e => e.sessionId)); return sessionIds.size; } calculateAverageSessionDuration(events) { // Would calculate based on session start/end events return 0; } calculateRetentionRate(events, start, end) { // Would calculate based on returning users return 0; } getDefaultPrivacySettings() { return { analyticsEnabled: false, // Opt-in by default errorReportingEnabled: false, performanceMetricsEnabled: false, featureUsageEnabled: false, shareAnonymousStats: false }; } async loadPrivacySettings() { // Would load from user configuration file return this.getDefaultPrivacySettings(); } savePrivacySettings(settings) { // Would save to user configuration file } clearStoredData() { this.events.clear(); this.emit('data:cleared'); } } exports.UsageAnalytics = UsageAnalytics; // Export factory functions for each platform const createCLIAnalytics = () => UsageAnalytics.getInstance('cli'); exports.createCLIAnalytics = createCLIAnalytics; const createWebAnalytics = () => UsageAnalytics.getInstance('web'); exports.createWebAnalytics = createWebAnalytics; const createExtensionAnalytics = () => UsageAnalytics.getInstance('extension'); exports.createExtensionAnalytics = createExtensionAnalytics; exports.default = UsageAnalytics; //# sourceMappingURL=usage-analytics.js.map