- Replace all kodo-* color classes across ~100 TSX files: kodo-void → background, kodo-ink → card, kodo-graphite → muted, kodo-steel → muted-foreground, kodo-cyan → primary, kodo-magenta → destructive, kodo-lime → success, kodo-red → destructive, kodo-gold → warning - Replace cyan-500, magenta-500, lime-500 default Tailwind colors with semantic equivalents (primary, destructive, success) - Fix WaveformVisualizer hardcoded hex colors to SUMI values - Delete global-effects.css (conflicting, redundant with index.css) Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
/**
|
|
* Monitor 3: User Analytics Service
|
|
* Tracks feature usage and user interactions
|
|
*/
|
|
|
|
import { apiClient } from './api/client';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
const MOCK_GLOBAL_STATS = {
|
|
total_users: 12500,
|
|
total_tracks: 3420,
|
|
total_plays: 1205430,
|
|
total_revenue: 14250.5,
|
|
followers: 24500,
|
|
profile_views: 45200,
|
|
trends: { plays: 8.2, revenue: 12.5, followers: 2.1, views: -2.4 },
|
|
sparklines: {
|
|
plays: [40, 35, 50, 60, 55, 70, 80, 75, 90],
|
|
revenue: [10, 12, 15, 14, 18, 20, 22, 25, 28],
|
|
followers: [20, 21, 21, 22, 22, 23, 23, 24, 24],
|
|
views: [50, 48, 45, 42, 40, 43, 41, 40, 38],
|
|
},
|
|
};
|
|
|
|
const TOP_TRACKS = [
|
|
{ id: 't1', title: 'Neon Nights', plays: 15420, change: 12, revenue: 145.5 },
|
|
{ id: 't2', title: 'Cyber City', plays: 12100, change: -5, revenue: 98.2 },
|
|
{ id: 't3', title: 'System Failure', plays: 8500, change: 24, revenue: 65.0 },
|
|
{ id: 't4', title: 'Mainframe', plays: 6200, change: 8, revenue: 42.1 },
|
|
];
|
|
|
|
export const analyticsService = {
|
|
/**
|
|
* Monitor 3: Record a user analytics event
|
|
* Tracks feature usage and user interactions
|
|
* @param eventName - Name of the event (e.g., 'feature_used', 'button_clicked')
|
|
* @param payload - Optional event data (e.g., { feature: 'search', query: '...' })
|
|
*/
|
|
recordEvent: async (eventName: string, payload?: Record<string, unknown>) => {
|
|
try {
|
|
// Send event to backend analytics endpoint
|
|
await apiClient.post('/analytics/events', {
|
|
event_name: eventName,
|
|
payload: payload || {},
|
|
});
|
|
|
|
// Log in development for debugging
|
|
if (import.meta.env.DEV) {
|
|
logger.debug('[Analytics] Event recorded', {
|
|
event_name: eventName,
|
|
payload,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
// Don't throw errors for analytics - fail silently to avoid disrupting user experience
|
|
if (import.meta.env.DEV) {
|
|
logger.warn('[Analytics] Failed to record event', {
|
|
event_name: eventName,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
getGlobalStats: async (range: string = '30d') => {
|
|
try {
|
|
// Action 1.1: Connect to real backend analytics
|
|
const response = await apiClient.get<any>('/analytics', {
|
|
params: { days: range.replace('d', '') }
|
|
});
|
|
|
|
// Transform backend data to UI format if needed
|
|
// If backend is empty, fallback to mock but log it
|
|
if (!response.data || Object.keys(response.data).length === 0) {
|
|
if (import.meta.env.DEV) logger.info('[Analytics] Backend returned empty stats, using baseline defaults');
|
|
return MOCK_GLOBAL_STATS;
|
|
}
|
|
|
|
return {
|
|
...MOCK_GLOBAL_STATS, // Use as baseline for fields not yet in backend
|
|
...response.data,
|
|
};
|
|
} catch (error) {
|
|
logger.error('[Analytics] Failed to fetch global stats', { error });
|
|
return MOCK_GLOBAL_STATS;
|
|
}
|
|
},
|
|
|
|
getTopTracks: async (range: string = '30d') => {
|
|
try {
|
|
const response = await apiClient.get<any[]>('/analytics/tracks/top', {
|
|
params: { days: range.replace('d', '') }
|
|
});
|
|
|
|
if (!response.data || response.data.length === 0) {
|
|
return TOP_TRACKS;
|
|
}
|
|
|
|
return response.data;
|
|
} catch (error) {
|
|
logger.error('[Analytics] Failed to fetch top tracks', { error });
|
|
return TOP_TRACKS;
|
|
}
|
|
},
|
|
|
|
getTrafficSources: async () => {
|
|
// Traffic sources might not be in backend yet, keep mock but ready for API
|
|
return [
|
|
{ label: 'Direct', val: 45, color: 'bg-primary' },
|
|
{ label: 'Social Media', val: 30, color: 'bg-destructive' },
|
|
{ label: 'Search', val: 15, color: 'bg-success' },
|
|
{ label: 'Referral', val: 10, color: 'bg-warning' },
|
|
];
|
|
},
|
|
|
|
getDeviceBreakdown: async () => {
|
|
return { mobile: 65, desktop: 35 };
|
|
},
|
|
};
|