UNPKG

@endlessblink/like-i-said-v2

Version:

Task Management & Memory for Claude - Track tasks, remember context, and maintain continuity across sessions with 27 powerful tools. Works with Claude Desktop and Claude Code.

355 lines (306 loc) 10.7 kB
/** * Privacy-First Analytics and Telemetry System * * This module provides optional, anonymous usage analytics to help improve * the Like-I-Said Memory Server. All data is anonymized and users can * opt-out at any time. * * Data Collection Principles: * 1. Anonymous - No personal information * 2. Minimal - Only essential usage metrics * 3. Transparent - Clear about what's collected * 4. Opt-in - Disabled by default, user must enable * 5. Secure - Encrypted transmission */ import { randomUUID } from 'crypto'; import fs from 'fs'; import path from 'path'; import os from 'os'; const ANALYTICS_VERSION = '1.0.0'; const ANALYTICS_ENDPOINT = 'https://analytics.like-i-said.dev/v1/events'; const CONFIG_FILE = 'analytics-config.json'; class AnalyticsCollector { constructor(dataDir = './data') { this.dataDir = dataDir; this.configPath = path.join(dataDir, CONFIG_FILE); this.config = this.loadConfig(); this.sessionId = randomUUID(); this.installationId = this.getInstallationId(); } /** * Load analytics configuration */ loadConfig() { try { if (fs.existsSync(this.configPath)) { const config = JSON.parse(fs.readFileSync(this.configPath, 'utf8')); return { enabled: false, // Default to disabled installationId: config.installationId || randomUUID(), firstRun: config.firstRun || new Date().toISOString(), optInDate: config.optInDate || null, ...config }; } } catch (error) { console.error('Analytics config not found, creating default...'); } // Default configuration const defaultConfig = { enabled: false, installationId: randomUUID(), firstRun: new Date().toISOString(), optInDate: null, version: ANALYTICS_VERSION }; this.saveConfig(defaultConfig); return defaultConfig; } /** * Save analytics configuration */ saveConfig(config = this.config) { try { if (!fs.existsSync(this.dataDir)) { fs.mkdirSync(this.dataDir, { recursive: true }); } fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2)); } catch (error) { console.error('Failed to save analytics config:', error.message); } } /** * Get anonymous installation ID */ getInstallationId() { return this.config.installationId; } /** * Enable analytics with user consent */ enableAnalytics() { this.config.enabled = true; this.config.optInDate = new Date().toISOString(); this.saveConfig(); // Send opt-in event this.trackEvent('analytics_enabled', { opt_in_date: this.config.optInDate, first_run: this.config.firstRun }); return true; } /** * Disable analytics */ disableAnalytics() { this.config.enabled = false; this.config.optInDate = null; this.saveConfig(); // Send final opt-out event this.trackEvent('analytics_disabled', { opt_out_date: new Date().toISOString() }); return true; } /** * Check if analytics is enabled */ isEnabled() { return this.config.enabled === true; } /** * Get system information (anonymous) */ getSystemInfo() { return { platform: os.platform(), arch: os.arch(), node_version: process.version, installation_type: 'manual', timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }; } /** * Track an event */ async trackEvent(eventName, properties = {}) { if (!this.isEnabled()) { return false; } const event = { event: eventName, installation_id: this.installationId, session_id: this.sessionId, timestamp: new Date().toISOString(), version: ANALYTICS_VERSION, system: this.getSystemInfo(), properties: { ...properties, // Ensure no PII is included memory_count: typeof properties.memory_count === 'number' ? Math.min(properties.memory_count, 10000) : undefined, task_count: typeof properties.task_count === 'number' ? Math.min(properties.task_count, 1000) : undefined } }; try { await this.sendEvent(event); return true; } catch (error) { console.error('Analytics event failed:', error.message); return false; } } /** * Send event to analytics endpoint */ async sendEvent(event) { if (process.env.NODE_ENV === 'test') { console.error('Analytics event (test mode):', event); return; } try { const response = await fetch(ANALYTICS_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': `like-i-said-v2/${ANALYTICS_VERSION}` }, body: JSON.stringify(event), timeout: 5000 // 5 second timeout }); if (!response.ok) { throw new Error(`Analytics server responded with ${response.status}`); } } catch (error) { // Fail silently - analytics should never break the main application if (process.env.DEBUG) { console.error('Analytics send failed:', error.message); } } } // Removed deprecated DXT installation tracking /** * Track manual installation */ async trackManualInstallation() { return this.trackEvent('manual_installation', { installation_method: 'npm', installation_time: Date.now(), package_version: process.env.PACKAGE_VERSION || 'unknown' }); } /** * Track tool usage */ async trackToolUsage(toolName, properties = {}) { return this.trackEvent('tool_used', { tool_name: toolName, ...properties }); } /** * Track memory operations */ async trackMemoryOperation(operation, properties = {}) { return this.trackEvent('memory_operation', { operation: operation, // add, search, list, delete, etc. ...properties }); } /** * Track task operations */ async trackTaskOperation(operation, properties = {}) { return this.trackEvent('task_operation', { operation: operation, // create, update, list, delete, etc. ...properties }); } /** * Track error events */ async trackError(errorType, properties = {}) { return this.trackEvent('error_occurred', { error_type: errorType, ...properties }); } /** * Track session summary */ async trackSessionSummary(summary = {}) { return this.trackEvent('session_summary', { session_duration: summary.duration || 0, tools_used: summary.toolsUsed || 0, memories_accessed: summary.memoriesAccessed || 0, tasks_modified: summary.tasksModified || 0, errors_encountered: summary.errorsEncountered || 0 }); } /** * Get analytics status for user display */ getStatus() { return { enabled: this.config.enabled, installationId: this.installationId, firstRun: this.config.firstRun, optInDate: this.config.optInDate, version: ANALYTICS_VERSION }; } /** * Show privacy notice */ getPrivacyNotice() { return { title: "Optional Analytics", message: `Like-I-Said can collect anonymous usage data to help improve the software. What we collect: • Tool usage patterns (which tools you use, not your data) • Performance metrics (response times, error rates) • System information (OS, Node.js version, installation type) • Usage frequency (how often you use different features) What we DON'T collect: • Your memories or task content • Personal information • File paths or names • IP addresses or location data All data is anonymous and cannot be traced back to you. You can opt-out at any time by running: "disable analytics" Would you like to enable anonymous analytics to help improve Like-I-Said?`, options: { enable: "Yes, help improve Like-I-Said", disable: "No, keep it private", learn_more: "Learn more about our privacy policy" } }; } } // Create global instance const analytics = new AnalyticsCollector(); // Export convenience functions export default analytics; export const trackEvent = (eventName, properties) => analytics.trackEvent(eventName, properties); // Removed deprecated DXT installation export export const trackManualInstallation = () => analytics.trackManualInstallation(); export const trackToolUsage = (toolName, properties) => analytics.trackToolUsage(toolName, properties); export const trackMemoryOperation = (operation, properties) => analytics.trackMemoryOperation(operation, properties); export const trackTaskOperation = (operation, properties) => analytics.trackTaskOperation(operation, properties); export const trackError = (errorType, properties) => analytics.trackError(errorType, properties); export const enableAnalytics = () => analytics.enableAnalytics(); export const disableAnalytics = () => analytics.disableAnalytics(); export const isAnalyticsEnabled = () => analytics.isEnabled(); export const getAnalyticsStatus = () => analytics.getStatus(); export const getPrivacyNotice = () => analytics.getPrivacyNotice();