WellNuo/services/__tests__/analytics.test.ts
Sergei 1e9ebd14ff Add sensor setup analytics tracking
Implemented comprehensive analytics system for tracking sensor setup process
including scan events, setup steps, and completion metrics.

Features:
- Analytics service with event tracking for sensor setup
- Metrics calculation (success rate, duration, common errors)
- Integration in add-sensor and setup-wifi screens
- Tracks: scan start/complete, setup start/complete, individual steps,
  retries, skips, and cancellations
- Comprehensive test coverage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 16:20:48 -08:00

359 lines
11 KiB
TypeScript

/**
* 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
});
});
});