recoder-analytics
Version:
Comprehensive analytics and monitoring for the Recoder.xyz ecosystem
464 lines • 16.9 kB
JavaScript
"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