/** * Usage Analytics Data Store * * Manages AI usage analytics and performance metrics * * @package ThinkRank * @since 1.0.0 */ import { createReduxStore } from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; // Initial state const DEFAULT_STATE = { metrics: {}, reports: {}, costs: {}, usage: {}, isLoading: false, error: null, lastUpdated: null, }; // Action types const TYPES = { SET_METRICS: 'SET_METRICS', SET_REPORTS: 'SET_REPORTS', SET_COSTS: 'SET_COSTS', SET_USAGE: 'SET_USAGE', SET_LOADING: 'SET_LOADING', SET_ERROR: 'SET_ERROR', SET_LAST_UPDATED: 'SET_LAST_UPDATED', CLEAR_ERROR: 'CLEAR_ERROR', }; // Reducer const reducer = (state = DEFAULT_STATE, action) => { switch (action.type) { case TYPES.SET_METRICS: return { ...state, metrics: action.metrics, error: null, // Clear error on successful data fetch }; case TYPES.SET_REPORTS: return { ...state, reports: action.reports, error: null, }; case TYPES.SET_COSTS: return { ...state, costs: action.costs, error: null, }; case TYPES.SET_USAGE: return { ...state, usage: action.usage, error: null, }; case TYPES.SET_LOADING: return { ...state, isLoading: action.loading, }; case TYPES.SET_ERROR: return { ...state, error: action.error, isLoading: false, // Stop loading on error }; case TYPES.CLEAR_ERROR: return { ...state, error: null, }; case TYPES.SET_LAST_UPDATED: return { ...state, lastUpdated: action.timestamp, }; default: return state; } }; // Actions const actions = { setMetrics(metrics) { return { type: TYPES.SET_METRICS, metrics, }; }, setReports(reports) { return { type: TYPES.SET_REPORTS, reports, }; }, setCosts(costs) { return { type: TYPES.SET_COSTS, costs, }; }, setUsage(usage) { return { type: TYPES.SET_USAGE, usage, }; }, setLoading(loading) { return { type: TYPES.SET_LOADING, loading, }; }, setError(error) { return { type: TYPES.SET_ERROR, error, }; }, clearError() { return { type: TYPES.CLEAR_ERROR, }; }, setLastUpdated(timestamp) { return { type: TYPES.SET_LAST_UPDATED, timestamp, }; }, // Simple action that returns a function for async operations fetchMetrics: (period = '30d', userId = 0) => { return async ({ dispatch }) => { dispatch(actions.setLoading(true)); dispatch(actions.clearError()); try { const response = await apiFetch({ path: `/thinkrank/v1/analytics/overview?period=${period}&user_id=${userId}`, }); if (response && response.success && response.data) { dispatch(actions.setMetrics(response.data)); dispatch(actions.setLastUpdated(Date.now())); } else { throw new Error(response?.message || 'Failed to fetch analytics data'); } } catch (error) { console.error('Failed to fetch analytics metrics:', error); let errorMessage = 'Failed to load analytics data'; if (error.message) { if (error.message.includes('404')) { errorMessage = 'Analytics API endpoint not found.'; } else if (error.message.includes('403')) { errorMessage = 'You do not have permission to view analytics data.'; } else if (error.message.includes('500')) { errorMessage = 'Server error while loading analytics data.'; } else { errorMessage = error.message; } } dispatch(actions.setError({ message: errorMessage, code: error.code || 'fetch_error', type: 'metrics' })); } finally { dispatch(actions.setLoading(false)); } }; }, fetchUsageBreakdown: (period = '30d', groupBy = 'day', userId = 0) => { return async ({ dispatch }) => { dispatch(actions.setLoading(true)); dispatch(actions.clearError()); try { const response = await apiFetch({ path: `/thinkrank/v1/analytics/usage?period=${period}&group_by=${groupBy}&user_id=${userId}`, }); if (response && response.success && response.data) { dispatch(actions.setUsage(response.data)); } else { throw new Error(response?.message || 'Failed to fetch usage data'); } } catch (error) { console.error('Failed to fetch usage breakdown:', error); dispatch(actions.setError({ message: error.message || 'Failed to load usage data', code: error.code || 'fetch_error', type: 'usage' })); } finally { dispatch(actions.setLoading(false)); } }; }, fetchCostAnalysis: (period = '30d', provider = 'all', userId = 0) => { return async ({ dispatch }) => { dispatch(actions.setLoading(true)); dispatch(actions.clearError()); try { const response = await apiFetch({ path: `/thinkrank/v1/analytics/costs?period=${period}&provider=${provider}&user_id=${userId}`, }); if (response && response.success && response.data) { dispatch(actions.setCosts(response.data)); } else { throw new Error(response?.message || 'Failed to fetch cost data'); } } catch (error) { console.error('Failed to fetch cost analysis:', error); dispatch(actions.setError({ message: error.message || 'Failed to load cost data', code: error.code || 'fetch_error', type: 'costs' })); } finally { dispatch(actions.setLoading(false)); } }; }, fetchReports: (type = 'overview') => { return async ({ dispatch }) => { dispatch(actions.setLoading(true)); dispatch(actions.clearError()); try { // Placeholder for future reports implementation const reports = { type, message: 'Reports feature coming in Phase 4' }; dispatch(actions.setReports(reports)); } catch (error) { console.error('Failed to fetch reports:', error); dispatch(actions.setError({ message: error.message || 'Failed to load reports', code: error.code || 'fetch_error', type: 'reports' })); } finally { dispatch(actions.setLoading(false)); } }; }, }; // Selectors const selectors = { // Basic state selectors getMetrics(state) { return state.metrics; }, getReports(state) { return state.reports; }, getCosts(state) { return state.costs; }, getUsage(state) { return state.usage; }, getIsLoading(state) { return state.isLoading; }, getError(state) { return state.error; }, getLastUpdated(state) { return state.lastUpdated; }, // Metrics selectors getContentOptimized(state) { return state.metrics.content_optimized || 0; }, getAverageSeoScore(state) { return state.metrics.average_seo_score || 0; }, getCreditsUsed(state) { return state.metrics.credits_used || 0; }, getTimeSaved(state) { return state.metrics.time_saved || 0; }, getAiActions(state) { return state.metrics.ai_actions || 0; }, getContentBriefs(state) { return state.metrics.content_briefs || 0; }, getFeaturesUsedCount(state) { return state.metrics.features_used_count || 0; }, getMostUsedFeature(state) { return state.metrics.most_used_feature || ''; }, getMostUsedCount(state) { return state.metrics.most_used_count || 0; }, getSuccessRate(state) { return state.metrics.success_rate || 0; }, getFeatureBreakdown(state) { return state.metrics.feature_breakdown || {}; }, getProviderBreakdown(state) { return state.metrics.provider_breakdown || {}; }, // Cost-related selectors getTotalCosts(state) { return state.metrics.total_cost || 0; }, getCostsByProvider(state) { const breakdown = state.metrics.provider_breakdown || {}; return { openai: breakdown.openai?.cost || 0, claude: breakdown.claude?.cost || 0 }; }, getEstimatedMonthlyCost(state) { const totalCost = state.metrics.total_cost || 0; const period = state.metrics.period || '30d'; // Calculate monthly projection based on current period switch (period) { case '7d': return totalCost * (30 / 7); case '30d': return totalCost; case '90d': return totalCost / 3; default: return totalCost; } }, getCostEfficiency(state) { const totalCost = state.metrics.total_cost || 0; const contentOptimized = state.metrics.content_optimized || 0; const avgScore = state.metrics.average_seo_score || 0; return { costPerOptimizedPost: contentOptimized > 0 ? totalCost / contentOptimized : 0, costPerSeoPoint: avgScore > 0 ? totalCost / avgScore : 0, timeSavedValue: (state.metrics.time_saved || 0) * 0.5 // Assuming $30/hour, 0.5 per minute }; }, // Usage trend selectors getUsageTrend(state) { return state.usage.trend || []; }, getFeatureUsageBreakdown(state) { return state.usage.by_feature || {}; }, // Error handling selectors hasError(state) { return !!state.error; }, getErrorMessage(state) { return state.error?.message || ''; }, getErrorType(state) { return state.error?.type || ''; }, // Data availability selectors hasData(state) { const metrics = state.metrics; return !!(metrics.content_optimized || metrics.ai_actions || metrics.total_cost); }, isDataStale(state) { if (!state.lastUpdated) return true; const fiveMinutes = 5 * 60 * 1000; return Date.now() - state.lastUpdated > fiveMinutes; }, // Combined selectors for dashboard cards (8-card layout) getDashboardMetrics(state) { // Format most used feature display const mostUsedFeature = state.metrics.most_used_feature || ''; const mostUsedCount = state.metrics.most_used_count || 0; // Get contextual subtitle based on feature type const getFeatureSubtitle = (feature, count) => { if (!feature || count === 0) return ''; // Map feature types to their action descriptions const actionMap = { 'content_brief': 'Generated', 'seo_metadata': 'Optimized', 'content_analysis': 'Analyzed', 'site_identity_optimization': 'Optimized', 'homepage_meta_optimization': 'Optimized', 'homepage_hero_optimization': 'Optimized', 'llms_txt_optimization': 'Generated' }; const action = actionMap[feature] || 'Used'; return `${count} ${action}`; }; const mostUsedDisplay = mostUsedFeature ? mostUsedFeature.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()).replace(/\bSeo\b/g, 'SEO') : 'None'; const mostUsedSubtitle = getFeatureSubtitle(mostUsedFeature, mostUsedCount); return { // Row 1: Core Performance Metrics postsAnalyzed: { value: state.metrics.content_optimized || 0, change: state.metrics.content_optimized_change || 0, label: 'Posts Analyzed' }, aiOptimizations: { value: state.metrics.ai_actions || 0, change: 0, // No change tracking for total actions yet label: 'AI Optimizations' }, averageSeoScore: { value: state.metrics.average_seo_score || 0, change: state.metrics.seo_score_change || 0, label: 'Average SEO Score', suffix: '/100' }, totalCost: { value: state.metrics.total_cost || 0, change: state.metrics.cost_change || 0, label: 'AI Credits Used', prefix: '$', subtitle: `${state.metrics.total_tokens || 0} tokens` }, // Row 2: Feature & Efficiency Metrics featuresUsed: { value: state.metrics.features_used_count || 0, change: 0, // No change tracking for features count yet label: 'Features Used' }, timeSaved: { value: Math.round((state.metrics.time_saved || 0) / 60), change: state.metrics.time_saved_change || 0, label: 'Time Saved', suffix: 'h', subtitle: `${state.metrics.ai_actions || 0} AI actions` }, mostUsedFeature: { value: mostUsedDisplay, change: 0, // No change tracking for most used feature yet label: 'Most Used Feature', subtitle: mostUsedSubtitle, isText: true }, successRate: { value: state.metrics.success_rate || 0, change: 0, // No change tracking for success rate yet label: 'Success Rate', suffix: '%' } }; }, }; // Create and export store const store = createReduxStore('thinkrank/analytics', { reducer, actions, selectors, }); export default store;