veza/apps/web/src/services/analyticsService.ts

160 lines
5.9 KiB
TypeScript
Raw Normal View History

/**
* 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<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 {
const response = await apiClient.get<{ data?: Record<string, unknown> }>('/analytics', {
params: { days: range.replace('d', '') }
});
const data = (response.data?.data ?? response.data) as Record<string, unknown> | 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<string, number> | undefined) ?? { plays: 0, revenue: 0, followers: 0, views: 0 },
sparklines: (data.sparklines as Record<string, number[]> | 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<string, unknown>)?.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<string, unknown> | undefined;
return data ?? {};
} catch (error) {
logger.error('[Analytics] Failed to fetch creator charts', { error });
throw error;
}
},
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<string, unknown> | 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 };
}
},
};