WellNuo/services/analytics.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

261 lines
6.2 KiB
TypeScript

/**
* 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<string, any>;
}
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<string, any>): 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<string, number> = {};
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();