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>
261 lines
6.2 KiB
TypeScript
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();
|