/** * Monitor 3: User Analytics Service * Tracks feature usage and user interactions */ import { apiClient } from './api/client'; import { logger } from '@/utils/logger'; 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) => { 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 { const response = await apiClient.get<{ data?: Record }>('/analytics', { params: { days: range.replace('d', '') } }); const data = (response.data?.data ?? response.data) as Record | undefined; if (!data || typeof data !== 'object') { return {}; } return { total_tracks: (data.total_tracks as number | undefined) ?? 0, total_plays: (data.total_plays as number | undefined) ?? 0, total_revenue: (data.total_revenue as number | undefined) ?? 0, followers: (data.followers as number | undefined) ?? 0, profile_views: (data.profile_views as number | undefined) ?? 0, trends: (data.trends as Record | undefined) ?? { plays: 0, revenue: 0, followers: 0, views: 0 }, sparklines: (data.sparklines as Record | undefined) ?? { plays: [0], revenue: [0], followers: [0], views: [0] }, }; } catch (error) { logger.error('[Analytics] Failed to fetch global stats', { error }); throw error; } }, getTopTracks: async (range: string = '30d') => { try { const response = await apiClient.get<{ data?: { tracks?: { top_tracks?: unknown[] } } }>('/analytics', { params: { days: range.replace('d', '') } }); const data = response.data?.data ?? response.data; const topTracks = (data as Record)?.tracks as { top_tracks?: Array<{ id?: string; title?: string; plays?: number; play_count?: number; change?: number; revenue?: number }> } | undefined; const tracks = topTracks?.top_tracks ?? []; return tracks.map((t) => ({ id: t.id ?? '', title: t.title ?? '', plays: t.plays ?? t.play_count ?? 0, change: t.change ?? 0, revenue: t.revenue ?? 0, })); } catch (error) { logger.error('[Analytics] Failed to fetch top tracks', { error }); throw error; } }, getTrafficSources: async () => { try { const response = await apiClient.get<{ sources?: unknown[] }>('/analytics/traffic-sources'); return (response.data?.sources ?? []) as unknown[]; } catch (error) { logger.warn('[Analytics] Failed to fetch traffic sources', { error }); return []; } }, getCreatorCharts: async (params?: { days?: number }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); const qs = searchParams.toString(); const url = `/analytics/creator/charts${qs ? `?${qs}` : ''}`; const response = await apiClient.get<{ data?: { plays_per_day?: Array<{ date: string; count: number }>; top_tracks?: Array<{ track_id: string; title: string; plays: number }>; period?: { start_date?: string; end_date?: string; days?: number }; }; }>(url); const data = (response.data?.data ?? response.data) as Record | undefined; return data ?? {}; } catch (error) { logger.error('[Analytics] Failed to fetch creator charts', { error }); throw error; } }, getCreatorExport: async (format: 'csv' | 'json', params?: { days?: number }) => { const searchParams = new URLSearchParams({ format }); if (params?.days != null) searchParams.set('days', String(params.days)); const url = `/analytics/creator/export?${searchParams.toString()}`; const response = await apiClient.get(url, { responseType: 'blob' }); return response.data as Blob; }, getCreatorStats: async (params?: { start_date?: string; end_date?: string; days?: number }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); if (params?.start_date) searchParams.set('start_date', params.start_date); if (params?.end_date) searchParams.set('end_date', params.end_date); const qs = searchParams.toString(); const url = `/analytics/creator/stats${qs ? `?${qs}` : ''}`; const response = await apiClient.get<{ data?: { total_plays?: number; unique_listeners?: number; average_completion_rate?: number; plays_by_day?: number[]; period?: { start_date?: string; end_date?: string; days?: number }; }; }>(url); const data = (response.data?.data ?? response.data) as Record | undefined; return data ?? {}; } catch (error) { logger.error('[Analytics] Failed to fetch creator stats', { error }); throw error; } }, getDeviceBreakdown: async () => { try { const response = await apiClient.get<{ mobile?: number; desktop?: number }>('/analytics/device-breakdown'); return { mobile: response.data?.mobile ?? 0, desktop: response.data?.desktop ?? 0, }; } catch (error) { logger.warn('[Analytics] Failed to fetch device breakdown', { error }); return { mobile: 0, desktop: 0 }; } }, // v0.11.0: Creator Analytics (F381-F395) getCreatorDashboard: async (params?: { days?: number; start_date?: string; end_date?: string }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); if (params?.start_date) searchParams.set('start_date', params.start_date); if (params?.end_date) searchParams.set('end_date', params.end_date); const qs = searchParams.toString(); const response = await apiClient.get(`/creator/analytics/dashboard${qs ? `?${qs}` : ''}`); const data = (response.data?.data ?? response.data) as Record; return data?.dashboard ?? data ?? {}; } catch (error) { logger.error('[Analytics] Failed to fetch creator dashboard', { error }); throw error; } }, getPlayEvolution: async (params?: { days?: number; interval?: string }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); if (params?.interval) searchParams.set('interval', params.interval); const qs = searchParams.toString(); const response = await apiClient.get(`/creator/analytics/plays${qs ? `?${qs}` : ''}`); const data = (response.data?.data ?? response.data) as Record; return (data?.data ?? []) as Array<{ date: string; total_plays: number; complete_listens: number; unique_listeners: number; total_play_time: number; avg_completion: number; }>; } catch (error) { logger.error('[Analytics] Failed to fetch play evolution', { error }); throw error; } }, getCreatorSales: async (params?: { days?: number }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); const qs = searchParams.toString(); const response = await apiClient.get(`/creator/analytics/sales${qs ? `?${qs}` : ''}`); const data = (response.data?.data ?? response.data) as Record; return (data?.sales ?? data ?? {}) as { total_revenue?: number; total_sales?: number; revenue_by_period?: Array<{ date: string; revenue: number; sales: number }>; top_selling_tracks?: Array<{ track_id: string; title: string; revenue: number; sales: number }>; }; } catch (error) { logger.error('[Analytics] Failed to fetch creator sales', { error }); throw error; } }, getDiscoverySources: async (params?: { days?: number }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); const qs = searchParams.toString(); const response = await apiClient.get(`/creator/analytics/discovery${qs ? `?${qs}` : ''}`); const data = (response.data?.data ?? response.data) as Record; return (data?.sources ?? []) as Array<{ source: string; count: number; percentage: number }>; } catch (error) { logger.warn('[Analytics] Failed to fetch discovery sources', { error }); return []; } }, getGeographicBreakdown: async (params?: { days?: number }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); const qs = searchParams.toString(); const response = await apiClient.get(`/creator/analytics/geographic${qs ? `?${qs}` : ''}`); const data = (response.data?.data ?? response.data) as Record; return (data?.geographic ?? []) as Array<{ country_code: string; play_count: number; unique_listeners: number; percentage: number; }>; } catch (error) { logger.warn('[Analytics] Failed to fetch geographic breakdown', { error }); return []; } }, getAudienceProfile: async (params?: { days?: number }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); const qs = searchParams.toString(); const response = await apiClient.get(`/creator/analytics/audience${qs ? `?${qs}` : ''}`); const data = (response.data?.data ?? response.data) as Record; return (data?.audience ?? data ?? {}) as { total_listeners?: number; listeners_by_genre?: Array<{ genre: string; count: number; percentage: number }>; top_listening_times?: Array<{ hour: number; play_count: number }>; }; } catch (error) { logger.warn('[Analytics] Failed to fetch audience profile', { error }); return {}; } }, getLiveMetrics: async (streamId: string) => { try { const response = await apiClient.get(`/creator/analytics/live/${streamId}`); const data = (response.data?.data ?? response.data) as Record; return (data?.metrics ?? data ?? {}) as { stream_id?: string; current_viewers?: number; peak_viewers?: number; is_live?: boolean; duration_seconds?: number; }; } catch (error) { logger.warn('[Analytics] Failed to fetch live metrics', { error }); return {}; } }, getCreatorTrackStats: async (params?: { days?: number; page?: number; limit?: number }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); if (params?.page != null) searchParams.set('page', String(params.page)); if (params?.limit != null) searchParams.set('limit', String(params.limit)); const qs = searchParams.toString(); const response = await apiClient.get(`/creator/analytics/tracks${qs ? `?${qs}` : ''}`); const data = (response.data?.data ?? response.data) as Record; return { tracks: (data?.data ?? []) as Array<{ track_id: string; title: string; total_plays: number; complete_listens: number; unique_listeners: number; avg_play_duration: number; avg_completion: number; }>, pagination: (data?.pagination ?? {}) as { page?: number; limit?: number; total?: number; total_pages?: number; }, }; } catch (error) { logger.error('[Analytics] Failed to fetch creator track stats', { error }); throw error; } }, exportCreatorAnalytics: async (type: 'plays' | 'sales', params?: { days?: number }) => { const searchParams = new URLSearchParams({ type }); if (params?.days != null) searchParams.set('days', String(params.days)); const response = await apiClient.get(`/creator/analytics/export?${searchParams.toString()}`, { responseType: 'blob', }); return response.data as Blob; }, // v0.11.1: Advanced Analytics (F396-F399) getTrackHeatmap: async (trackId: string, params?: { days?: number }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); const qs = searchParams.toString(); const response = await apiClient.get(`/creator/analytics/heatmap/${trackId}${qs ? `?${qs}` : ''}`); const data = (response.data?.data ?? response.data) as Record; return (data?.heatmap ?? {}) as { track_id?: string; total_segments?: number; segment_duration_ms?: number; segments?: Array<{ segment_index: number; segment_start_ms: number; segment_end_ms: number; listen_count: number; drop_off_count: number; replay_count: number; intensity: number; }>; max_listens?: number; avg_drop_off?: number; }; } catch (error) { logger.warn('[Analytics] Failed to fetch track heatmap', { error }); return {}; } }, comparePeriods: async (params?: { preset?: string; current_start?: string; current_end?: string; previous_start?: string; previous_end?: string }) => { try { const searchParams = new URLSearchParams(); if (params?.preset) searchParams.set('preset', params.preset); if (params?.current_start) searchParams.set('current_start', params.current_start); if (params?.current_end) searchParams.set('current_end', params.current_end); if (params?.previous_start) searchParams.set('previous_start', params.previous_start); if (params?.previous_end) searchParams.set('previous_end', params.previous_end); const qs = searchParams.toString(); const response = await apiClient.get(`/creator/analytics/compare${qs ? `?${qs}` : ''}`); const data = (response.data?.data ?? response.data) as Record; return (data?.comparison ?? {}) as { current_period?: { start_date: string; end_date: string; total_plays: number; unique_listeners: number; complete_listens: number; total_play_time: number; avg_completion: number; total_revenue: number; new_followers: number; }; previous_period?: { start_date: string; end_date: string; total_plays: number; unique_listeners: number; complete_listens: number; total_play_time: number; avg_completion: number; total_revenue: number; new_followers: number; }; changes?: { plays_change: number; listeners_change: number; completion_change: number; revenue_change: number; followers_change: number; play_time_change: number; }; }; } catch (error) { logger.warn('[Analytics] Failed to compare periods', { error }); return {}; } }, getMarketplaceAnalytics: async (params?: { days?: number }) => { try { const searchParams = new URLSearchParams(); if (params?.days != null) searchParams.set('days', String(params.days)); const qs = searchParams.toString(); const response = await apiClient.get(`/creator/analytics/marketplace${qs ? `?${qs}` : ''}`); const data = (response.data?.data ?? response.data) as Record; return (data?.marketplace ?? {}) as { total_views?: number; total_sales?: number; total_revenue?: number; overall_conversion?: number; platform_commission?: number; net_revenue?: number; products?: Array<{ product_id: string; name: string; product_type: string; views: number; sales: number; revenue: number; conversion_rate: number; }>; revenue_timeline?: Array<{ date: string; revenue: number; sales: number; views: number }>; }; } catch (error) { logger.warn('[Analytics] Failed to fetch marketplace analytics', { error }); return {}; } }, getMetricAlerts: async () => { try { const response = await apiClient.get('/creator/analytics/alerts'); const data = (response.data?.data ?? response.data) as Record; return (data?.alerts ?? {}) as { preferences?: Array<{ metric_type: string; enabled: boolean }>; alerts?: Array<{ id: string; metric_type: string; threshold: number; is_triggered: boolean; triggered_at?: string; created_at: string; }>; pending?: Array<{ id: string; metric_type: string; threshold: number; is_triggered: boolean; triggered_at?: string; created_at: string; }>; }; } catch (error) { logger.warn('[Analytics] Failed to fetch metric alerts', { error }); return {}; } }, createMetricAlert: async (metricType: string, threshold: number) => { const response = await apiClient.post('/creator/analytics/alerts', { metric_type: metricType, threshold, }); return response.data; }, updateAlertPreference: async (metricType: string, enabled: boolean) => { const response = await apiClient.put('/creator/analytics/alerts/preferences', { metric_type: metricType, enabled, }); return response.data; }, deleteMetricAlert: async (alertId: string) => { const response = await apiClient.delete(`/creator/analytics/alerts/${alertId}`); return response.data; }, checkMetricAlerts: async () => { const response = await apiClient.post('/creator/analytics/alerts/check'); return response.data; }, };