/** * Analytics Service * * Simple, lightweight analytics tracking for sensor setup and other app events. * Logs events locally for now - can be extended to send to analytics platforms * (Mixpanel, Amplitude, etc.) in the future. */ export interface AnalyticsEvent { name: string; timestamp: number; properties?: Record; } export interface SensorSetupStartProperties { beneficiaryId: string; sensorCount: number; scanDuration?: number; // milliseconds } export interface SensorSetupProgressProperties { beneficiaryId: string; sensorIndex: number; totalSensors: number; step: 'connect' | 'unlock' | 'wifi' | 'attach' | 'reboot'; stepStatus: 'started' | 'completed' | 'failed'; error?: string; } export interface SensorSetupCompleteProperties { beneficiaryId: string; totalSensors: number; successCount: number; failureCount: number; skippedCount: number; totalDuration: number; // milliseconds averageSensorSetupTime?: number; // milliseconds } export interface SensorScanProperties { beneficiaryId: string; scanDuration: number; // milliseconds sensorsFound: number; selectedCount: number; } class AnalyticsService { private events: AnalyticsEvent[] = []; private readonly maxStoredEvents = 1000; private isEnabled = true; /** * Track a generic event */ track(eventName: string, properties?: Record): void { if (!this.isEnabled) return; const event: AnalyticsEvent = { name: eventName, timestamp: Date.now(), properties, }; this.events.push(event); this.trimEvents(); // Log to console in development if (__DEV__) { console.log('[Analytics]', eventName, properties); } } // === Sensor Setup Events === /** * Track when user starts scanning for sensors */ trackSensorScanStart(beneficiaryId: string): void { this.track('sensor_scan_start', { beneficiaryId }); } /** * Track scan results */ trackSensorScanComplete(properties: SensorScanProperties): void { this.track('sensor_scan_complete', properties); } /** * Track when batch setup begins */ trackSensorSetupStart(properties: SensorSetupStartProperties): void { this.track('sensor_setup_start', properties); } /** * Track individual step progress */ trackSensorSetupStep(properties: SensorSetupProgressProperties): void { this.track('sensor_setup_step', properties); } /** * Track setup completion */ trackSensorSetupComplete(properties: SensorSetupCompleteProperties): void { this.track('sensor_setup_complete', properties); } /** * Track when user cancels setup */ trackSensorSetupCancelled(properties: { beneficiaryId: string; currentSensorIndex: number; totalSensors: number; reason?: string; }): void { this.track('sensor_setup_cancelled', properties); } /** * Track retry attempt */ trackSensorSetupRetry(properties: { beneficiaryId: string; sensorId: string; sensorName: string; previousError: string; }): void { this.track('sensor_setup_retry', properties); } /** * Track skip action */ trackSensorSetupSkip(properties: { beneficiaryId: string; sensorId: string; sensorName: string; error: string; }): void { this.track('sensor_setup_skip', properties); } // === Utility Methods === /** * Get all stored events */ getEvents(): AnalyticsEvent[] { return [...this.events]; } /** * Get events filtered by name */ getEventsByName(eventName: string): AnalyticsEvent[] { return this.events.filter(e => e.name === eventName); } /** * Get events within time range */ getEventsInRange(startTime: number, endTime: number): AnalyticsEvent[] { return this.events.filter( e => e.timestamp >= startTime && e.timestamp <= endTime ); } /** * Clear all stored events */ clearEvents(): void { this.events = []; } /** * Enable/disable tracking */ setEnabled(enabled: boolean): void { this.isEnabled = enabled; } /** * Trim events array to max size (FIFO) */ private trimEvents(): void { if (this.events.length > this.maxStoredEvents) { this.events = this.events.slice(-this.maxStoredEvents); } } /** * Get sensor setup metrics summary */ getSensorSetupMetrics(timeRangeMs?: number): { totalSetups: number; successRate: number; averageDuration: number; averageSensorsPerSetup: number; commonErrors: { error: string; count: number }[]; } { const now = Date.now(); const startTime = timeRangeMs ? now - timeRangeMs : 0; const setupEvents = this.getEventsInRange(startTime, now).filter( e => e.name === 'sensor_setup_complete' ); if (setupEvents.length === 0) { return { totalSetups: 0, successRate: 0, averageDuration: 0, averageSensorsPerSetup: 0, commonErrors: [], }; } const totalSetups = setupEvents.length; let totalSuccess = 0; let totalDuration = 0; let totalSensors = 0; const errorCounts: Record = {}; setupEvents.forEach(event => { const props = event.properties as SensorSetupCompleteProperties; totalSuccess += props.successCount; totalDuration += props.totalDuration; totalSensors += props.totalSensors; }); // Collect errors from step events const stepEvents = this.getEventsInRange(startTime, now).filter( e => e.name === 'sensor_setup_step' && e.properties?.stepStatus === 'failed' ); stepEvents.forEach(event => { const error = event.properties?.error || 'Unknown error'; errorCounts[error] = (errorCounts[error] || 0) + 1; }); const commonErrors = Object.entries(errorCounts) .map(([error, count]) => ({ error, count })) .sort((a, b) => b.count - a.count) .slice(0, 5); return { totalSetups, successRate: totalSensors > 0 ? (totalSuccess / totalSensors) * 100 : 0, averageDuration: totalSetups > 0 ? totalDuration / totalSetups : 0, averageSensorsPerSetup: totalSetups > 0 ? totalSensors / totalSetups : 0, commonErrors, }; } } // Export singleton instance export const analytics = new AnalyticsService();