/** * Analytics Service Tests */ import { analytics } from '../analytics'; describe('Analytics Service', () => { beforeEach(() => { // Clear events before each test analytics.clearEvents(); analytics.setEnabled(true); }); describe('Basic Event Tracking', () => { it('should track a generic event', () => { analytics.track('test_event', { foo: 'bar' }); const events = analytics.getEvents(); expect(events).toHaveLength(1); expect(events[0].name).toBe('test_event'); expect(events[0].properties).toEqual({ foo: 'bar' }); expect(events[0].timestamp).toBeGreaterThan(0); }); it('should track events without properties', () => { analytics.track('simple_event'); const events = analytics.getEvents(); expect(events).toHaveLength(1); expect(events[0].name).toBe('simple_event'); expect(events[0].properties).toBeUndefined(); }); it('should not track events when disabled', () => { analytics.setEnabled(false); analytics.track('disabled_event'); const events = analytics.getEvents(); expect(events).toHaveLength(0); }); }); describe('Sensor Scan Events', () => { it('should track sensor scan start', () => { analytics.trackSensorScanStart('beneficiary_123'); const events = analytics.getEvents(); expect(events).toHaveLength(1); expect(events[0].name).toBe('sensor_scan_start'); expect(events[0].properties).toEqual({ beneficiaryId: 'beneficiary_123' }); }); it('should track sensor scan completion', () => { analytics.trackSensorScanComplete({ beneficiaryId: 'beneficiary_123', scanDuration: 5000, sensorsFound: 3, selectedCount: 2, }); const events = analytics.getEvents(); expect(events).toHaveLength(1); expect(events[0].name).toBe('sensor_scan_complete'); expect(events[0].properties).toMatchObject({ beneficiaryId: 'beneficiary_123', scanDuration: 5000, sensorsFound: 3, selectedCount: 2, }); }); }); describe('Sensor Setup Events', () => { it('should track setup start', () => { analytics.trackSensorSetupStart({ beneficiaryId: 'beneficiary_123', sensorCount: 2, scanDuration: 5000, }); const events = analytics.getEvents(); expect(events).toHaveLength(1); expect(events[0].name).toBe('sensor_setup_start'); expect(events[0].properties).toMatchObject({ beneficiaryId: 'beneficiary_123', sensorCount: 2, scanDuration: 5000, }); }); it('should track setup steps', () => { analytics.trackSensorSetupStep({ beneficiaryId: 'beneficiary_123', sensorIndex: 0, totalSensors: 2, step: 'connect', stepStatus: 'started', }); analytics.trackSensorSetupStep({ beneficiaryId: 'beneficiary_123', sensorIndex: 0, totalSensors: 2, step: 'connect', stepStatus: 'completed', }); const events = analytics.getEvents(); expect(events).toHaveLength(2); expect(events[0].properties?.step).toBe('connect'); expect(events[0].properties?.stepStatus).toBe('started'); expect(events[1].properties?.stepStatus).toBe('completed'); }); it('should track setup step failures with error', () => { analytics.trackSensorSetupStep({ beneficiaryId: 'beneficiary_123', sensorIndex: 0, totalSensors: 2, step: 'wifi', stepStatus: 'failed', error: 'Connection timeout', }); const events = analytics.getEvents(); expect(events[0].properties?.error).toBe('Connection timeout'); }); it('should track setup completion', () => { analytics.trackSensorSetupComplete({ beneficiaryId: 'beneficiary_123', totalSensors: 3, successCount: 2, failureCount: 1, skippedCount: 0, totalDuration: 45000, averageSensorSetupTime: 15000, }); const events = analytics.getEvents(); expect(events).toHaveLength(1); expect(events[0].name).toBe('sensor_setup_complete'); expect(events[0].properties).toMatchObject({ totalSensors: 3, successCount: 2, failureCount: 1, skippedCount: 0, totalDuration: 45000, averageSensorSetupTime: 15000, }); }); it('should track retry attempts', () => { analytics.trackSensorSetupRetry({ beneficiaryId: 'beneficiary_123', sensorId: 'sensor_1', sensorName: 'WP_123', previousError: 'Connection failed', }); const events = analytics.getEvents(); expect(events).toHaveLength(1); expect(events[0].name).toBe('sensor_setup_retry'); expect(events[0].properties).toMatchObject({ sensorId: 'sensor_1', previousError: 'Connection failed', }); }); it('should track skip actions', () => { analytics.trackSensorSetupSkip({ beneficiaryId: 'beneficiary_123', sensorId: 'sensor_1', sensorName: 'WP_123', error: 'Connection failed', }); const events = analytics.getEvents(); expect(events).toHaveLength(1); expect(events[0].name).toBe('sensor_setup_skip'); }); it('should track cancellation', () => { analytics.trackSensorSetupCancelled({ beneficiaryId: 'beneficiary_123', currentSensorIndex: 1, totalSensors: 3, reason: 'user_cancelled', }); const events = analytics.getEvents(); expect(events).toHaveLength(1); expect(events[0].name).toBe('sensor_setup_cancelled'); expect(events[0].properties?.reason).toBe('user_cancelled'); }); }); describe('Event Filtering', () => { beforeEach(() => { analytics.track('event_a', { value: 1 }); analytics.track('event_b', { value: 2 }); analytics.track('event_a', { value: 3 }); }); it('should filter events by name', () => { const eventAEvents = analytics.getEventsByName('event_a'); expect(eventAEvents).toHaveLength(2); expect(eventAEvents[0].properties?.value).toBe(1); expect(eventAEvents[1].properties?.value).toBe(3); }); it('should return empty array for non-existent event name', () => { const events = analytics.getEventsByName('non_existent'); expect(events).toHaveLength(0); }); it('should filter events by time range', () => { const now = Date.now(); const events = analytics.getEventsInRange(now - 1000, now + 1000); expect(events).toHaveLength(3); }); it('should return empty array for out-of-range queries', () => { const now = Date.now(); const events = analytics.getEventsInRange(now - 10000, now - 5000); expect(events).toHaveLength(0); }); }); describe('Event Storage Management', () => { it('should clear all events', () => { analytics.track('event_1'); analytics.track('event_2'); expect(analytics.getEvents()).toHaveLength(2); analytics.clearEvents(); expect(analytics.getEvents()).toHaveLength(0); }); it('should trim events when exceeding max storage', () => { // Create 1000 events (the max) for (let i = 0; i < 1000; i++) { analytics.track(`event_${i}`); } expect(analytics.getEvents()).toHaveLength(1000); // Add one more - should trim oldest analytics.track('event_1001'); const events = analytics.getEvents(); expect(events).toHaveLength(1000); expect(events[0].name).toBe('event_1'); // First event was removed expect(events[events.length - 1].name).toBe('event_1001'); // Last event is newest }); }); describe('Sensor Setup Metrics', () => { beforeEach(() => { // Simulate successful setup analytics.trackSensorSetupComplete({ beneficiaryId: 'beneficiary_1', totalSensors: 3, successCount: 3, failureCount: 0, skippedCount: 0, totalDuration: 30000, averageSensorSetupTime: 10000, }); // Simulate partial failure analytics.trackSensorSetupComplete({ beneficiaryId: 'beneficiary_2', totalSensors: 2, successCount: 1, failureCount: 1, skippedCount: 0, totalDuration: 25000, }); // Add some step failures analytics.trackSensorSetupStep({ beneficiaryId: 'beneficiary_2', sensorIndex: 1, totalSensors: 2, step: 'wifi', stepStatus: 'failed', error: 'Connection timeout', }); analytics.trackSensorSetupStep({ beneficiaryId: 'beneficiary_2', sensorIndex: 1, totalSensors: 2, step: 'wifi', stepStatus: 'failed', error: 'Connection timeout', }); analytics.trackSensorSetupStep({ beneficiaryId: 'beneficiary_1', sensorIndex: 0, totalSensors: 3, step: 'attach', stepStatus: 'failed', error: 'API error', }); }); it('should calculate total setups', () => { const metrics = analytics.getSensorSetupMetrics(); expect(metrics.totalSetups).toBe(2); }); it('should calculate success rate', () => { const metrics = analytics.getSensorSetupMetrics(); // 4 successful out of 5 total sensors = 80% expect(metrics.successRate).toBe(80); }); it('should calculate average duration', () => { const metrics = analytics.getSensorSetupMetrics(); // (30000 + 25000) / 2 = 27500 expect(metrics.averageDuration).toBe(27500); }); it('should calculate average sensors per setup', () => { const metrics = analytics.getSensorSetupMetrics(); // (3 + 2) / 2 = 2.5 expect(metrics.averageSensorsPerSetup).toBe(2.5); }); it('should identify common errors', () => { const metrics = analytics.getSensorSetupMetrics(); expect(metrics.commonErrors).toHaveLength(2); expect(metrics.commonErrors[0].error).toBe('Connection timeout'); expect(metrics.commonErrors[0].count).toBe(2); expect(metrics.commonErrors[1].error).toBe('API error'); expect(metrics.commonErrors[1].count).toBe(1); }); it('should handle empty metrics gracefully', () => { analytics.clearEvents(); const metrics = analytics.getSensorSetupMetrics(); expect(metrics).toEqual({ totalSetups: 0, successRate: 0, averageDuration: 0, averageSensorsPerSetup: 0, commonErrors: [], }); }); it('should filter metrics by time range', () => { const now = Date.now(); const metrics = analytics.getSensorSetupMetrics(1000); // Last second expect(metrics.totalSetups).toBe(2); // All events are recent }); }); });