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>
359 lines
11 KiB
TypeScript
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
|
|
});
|
|
});
|
|
});
|