PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / src / admin / stores / usageAnalytics.js

usageAnalytics.js in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.0, at src/admin/stores/usageAnalytics.js

534 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Usage Analytics Data Store
3 *
4 * Manages AI usage analytics and performance metrics
5 *
6 * @package ThinkRank
7 * @since 1.0.0
8 */
9
10 import { createReduxStore } from '@wordpress/data';
11 import apiFetch from '@wordpress/api-fetch';
12
13 // Initial state
14 const DEFAULT_STATE = {
15 metrics: {},
16 reports: {},
17 costs: {},
18 usage: {},
19 isLoading: false,
20 error: null,
21 lastUpdated: null,
22 };
23
24 // Action types
25 const TYPES = {
26 SET_METRICS: 'SET_METRICS',
27 SET_REPORTS: 'SET_REPORTS',
28 SET_COSTS: 'SET_COSTS',
29 SET_USAGE: 'SET_USAGE',
30 SET_LOADING: 'SET_LOADING',
31 SET_ERROR: 'SET_ERROR',
32 SET_LAST_UPDATED: 'SET_LAST_UPDATED',
33 CLEAR_ERROR: 'CLEAR_ERROR',
34 };
35
36 // Reducer
37 const reducer = (state = DEFAULT_STATE, action) => {
38 switch (action.type) {
39 case TYPES.SET_METRICS:
40 return {
41 ...state,
42 metrics: action.metrics,
43 error: null, // Clear error on successful data fetch
44 };
45
46 case TYPES.SET_REPORTS:
47 return {
48 ...state,
49 reports: action.reports,
50 error: null,
51 };
52
53 case TYPES.SET_COSTS:
54 return {
55 ...state,
56 costs: action.costs,
57 error: null,
58 };
59
60 case TYPES.SET_USAGE:
61 return {
62 ...state,
63 usage: action.usage,
64 error: null,
65 };
66
67 case TYPES.SET_LOADING:
68 return {
69 ...state,
70 isLoading: action.loading,
71 };
72
73 case TYPES.SET_ERROR:
74 return {
75 ...state,
76 error: action.error,
77 isLoading: false, // Stop loading on error
78 };
79
80 case TYPES.CLEAR_ERROR:
81 return {
82 ...state,
83 error: null,
84 };
85
86 case TYPES.SET_LAST_UPDATED:
87 return {
88 ...state,
89 lastUpdated: action.timestamp,
90 };
91
92 default:
93 return state;
94 }
95 };
96
97 // Actions
98 const actions = {
99 setMetrics(metrics) {
100 return {
101 type: TYPES.SET_METRICS,
102 metrics,
103 };
104 },
105
106 setReports(reports) {
107 return {
108 type: TYPES.SET_REPORTS,
109 reports,
110 };
111 },
112
113 setCosts(costs) {
114 return {
115 type: TYPES.SET_COSTS,
116 costs,
117 };
118 },
119
120 setUsage(usage) {
121 return {
122 type: TYPES.SET_USAGE,
123 usage,
124 };
125 },
126
127 setLoading(loading) {
128 return {
129 type: TYPES.SET_LOADING,
130 loading,
131 };
132 },
133
134 setError(error) {
135 return {
136 type: TYPES.SET_ERROR,
137 error,
138 };
139 },
140
141 clearError() {
142 return {
143 type: TYPES.CLEAR_ERROR,
144 };
145 },
146
147 setLastUpdated(timestamp) {
148 return {
149 type: TYPES.SET_LAST_UPDATED,
150 timestamp,
151 };
152 },
153
154 // Simple action that returns a function for async operations
155 fetchMetrics: (period = '30d', userId = 0) => {
156 return async ({ dispatch }) => {
157 dispatch(actions.setLoading(true));
158 dispatch(actions.clearError());
159
160 try {
161 const response = await apiFetch({
162 path: `/thinkrank/v1/analytics/overview?period=${period}&user_id=${userId}`,
163 });
164
165 if (response && response.success && response.data) {
166 dispatch(actions.setMetrics(response.data));
167 dispatch(actions.setLastUpdated(Date.now()));
168 } else {
169 throw new Error(response?.message || 'Failed to fetch analytics data');
170 }
171
172 } catch (error) {
173 console.error('Failed to fetch analytics metrics:', error);
174
175 let errorMessage = 'Failed to load analytics data';
176 if (error.message) {
177 if (error.message.includes('404')) {
178 errorMessage = 'Analytics API endpoint not found.';
179 } else if (error.message.includes('403')) {
180 errorMessage = 'You do not have permission to view analytics data.';
181 } else if (error.message.includes('500')) {
182 errorMessage = 'Server error while loading analytics data.';
183 } else {
184 errorMessage = error.message;
185 }
186 }
187
188 dispatch(actions.setError({
189 message: errorMessage,
190 code: error.code || 'fetch_error',
191 type: 'metrics'
192 }));
193 } finally {
194 dispatch(actions.setLoading(false));
195 }
196 };
197 },
198
199 fetchUsageBreakdown: (period = '30d', groupBy = 'day', userId = 0) => {
200 return async ({ dispatch }) => {
201 dispatch(actions.setLoading(true));
202 dispatch(actions.clearError());
203
204 try {
205 const response = await apiFetch({
206 path: `/thinkrank/v1/analytics/usage?period=${period}&group_by=${groupBy}&user_id=${userId}`,
207 });
208
209 if (response && response.success && response.data) {
210 dispatch(actions.setUsage(response.data));
211 } else {
212 throw new Error(response?.message || 'Failed to fetch usage data');
213 }
214
215 } catch (error) {
216 console.error('Failed to fetch usage breakdown:', error);
217 dispatch(actions.setError({
218 message: error.message || 'Failed to load usage data',
219 code: error.code || 'fetch_error',
220 type: 'usage'
221 }));
222 } finally {
223 dispatch(actions.setLoading(false));
224 }
225 };
226 },
227
228 fetchCostAnalysis: (period = '30d', provider = 'all', userId = 0) => {
229 return async ({ dispatch }) => {
230 dispatch(actions.setLoading(true));
231 dispatch(actions.clearError());
232
233 try {
234 const response = await apiFetch({
235 path: `/thinkrank/v1/analytics/costs?period=${period}&provider=${provider}&user_id=${userId}`,
236 });
237
238 if (response && response.success && response.data) {
239 dispatch(actions.setCosts(response.data));
240 } else {
241 throw new Error(response?.message || 'Failed to fetch cost data');
242 }
243
244 } catch (error) {
245 console.error('Failed to fetch cost analysis:', error);
246 dispatch(actions.setError({
247 message: error.message || 'Failed to load cost data',
248 code: error.code || 'fetch_error',
249 type: 'costs'
250 }));
251 } finally {
252 dispatch(actions.setLoading(false));
253 }
254 };
255 },
256
257 fetchReports: (type = 'overview') => {
258 return async ({ dispatch }) => {
259 dispatch(actions.setLoading(true));
260 dispatch(actions.clearError());
261
262 try {
263 // Placeholder for future reports implementation
264 const reports = {
265 type,
266 message: 'Reports feature coming in Phase 4'
267 };
268
269 dispatch(actions.setReports(reports));
270
271 } catch (error) {
272 console.error('Failed to fetch reports:', error);
273 dispatch(actions.setError({
274 message: error.message || 'Failed to load reports',
275 code: error.code || 'fetch_error',
276 type: 'reports'
277 }));
278 } finally {
279 dispatch(actions.setLoading(false));
280 }
281 };
282 },
283 };
284
285 // Selectors
286 const selectors = {
287 // Basic state selectors
288 getMetrics(state) {
289 return state.metrics;
290 },
291
292 getReports(state) {
293 return state.reports;
294 },
295
296 getCosts(state) {
297 return state.costs;
298 },
299
300 getUsage(state) {
301 return state.usage;
302 },
303
304 getIsLoading(state) {
305 return state.isLoading;
306 },
307
308 getError(state) {
309 return state.error;
310 },
311
312 getLastUpdated(state) {
313 return state.lastUpdated;
314 },
315
316 // Metrics selectors
317 getContentOptimized(state) {
318 return state.metrics.content_optimized || 0;
319 },
320
321 getAverageSeoScore(state) {
322 return state.metrics.average_seo_score || 0;
323 },
324
325 getCreditsUsed(state) {
326 return state.metrics.credits_used || 0;
327 },
328
329 getTimeSaved(state) {
330 return state.metrics.time_saved || 0;
331 },
332
333 getAiActions(state) {
334 return state.metrics.ai_actions || 0;
335 },
336
337 getContentBriefs(state) {
338 return state.metrics.content_briefs || 0;
339 },
340
341 getFeaturesUsedCount(state) {
342 return state.metrics.features_used_count || 0;
343 },
344
345 getMostUsedFeature(state) {
346 return state.metrics.most_used_feature || '';
347 },
348
349 getMostUsedCount(state) {
350 return state.metrics.most_used_count || 0;
351 },
352
353 getSuccessRate(state) {
354 return state.metrics.success_rate || 0;
355 },
356
357 getFeatureBreakdown(state) {
358 return state.metrics.feature_breakdown || {};
359 },
360
361 getProviderBreakdown(state) {
362 return state.metrics.provider_breakdown || {};
363 },
364
365 // Cost-related selectors
366 getTotalCosts(state) {
367 return state.metrics.total_cost || 0;
368 },
369
370 getCostsByProvider(state) {
371 const breakdown = state.metrics.provider_breakdown || {};
372 return {
373 openai: breakdown.openai?.cost || 0,
374 claude: breakdown.claude?.cost || 0
375 };
376 },
377
378 getEstimatedMonthlyCost(state) {
379 const totalCost = state.metrics.total_cost || 0;
380 const period = state.metrics.period || '30d';
381
382 // Calculate monthly projection based on current period
383 switch (period) {
384 case '7d':
385 return totalCost * (30 / 7);
386 case '30d':
387 return totalCost;
388 case '90d':
389 return totalCost / 3;
390 default:
391 return totalCost;
392 }
393 },
394
395 getCostEfficiency(state) {
396 const totalCost = state.metrics.total_cost || 0;
397 const contentOptimized = state.metrics.content_optimized || 0;
398 const avgScore = state.metrics.average_seo_score || 0;
399
400 return {
401 costPerOptimizedPost: contentOptimized > 0 ? totalCost / contentOptimized : 0,
402 costPerSeoPoint: avgScore > 0 ? totalCost / avgScore : 0,
403 timeSavedValue: (state.metrics.time_saved || 0) * 0.5 // Assuming $30/hour, 0.5 per minute
404 };
405 },
406
407 // Usage trend selectors
408 getUsageTrend(state) {
409 return state.usage.trend || [];
410 },
411
412 getFeatureUsageBreakdown(state) {
413 return state.usage.by_feature || {};
414 },
415
416 // Error handling selectors
417 hasError(state) {
418 return !!state.error;
419 },
420
421 getErrorMessage(state) {
422 return state.error?.message || '';
423 },
424
425 getErrorType(state) {
426 return state.error?.type || '';
427 },
428
429 // Data availability selectors
430 hasData(state) {
431 const metrics = state.metrics;
432 return !!(metrics.content_optimized || metrics.ai_actions || metrics.total_cost);
433 },
434
435 isDataStale(state) {
436 if (!state.lastUpdated) return true;
437 const fiveMinutes = 5 * 60 * 1000;
438 return Date.now() - state.lastUpdated > fiveMinutes;
439 },
440
441 // Combined selectors for dashboard cards (8-card layout)
442 getDashboardMetrics(state) {
443 // Format most used feature display
444 const mostUsedFeature = state.metrics.most_used_feature || '';
445 const mostUsedCount = state.metrics.most_used_count || 0;
446
447 // Get contextual subtitle based on feature type
448 const getFeatureSubtitle = (feature, count) => {
449 if (!feature || count === 0) return '';
450
451 // Map feature types to their action descriptions
452 const actionMap = {
453 'content_brief': 'Generated',
454 'seo_metadata': 'Optimized',
455 'content_analysis': 'Analyzed',
456 'site_identity_optimization': 'Optimized',
457 'homepage_meta_optimization': 'Optimized',
458 'homepage_hero_optimization': 'Optimized',
459 'llms_txt_optimization': 'Generated'
460 };
461
462 const action = actionMap[feature] || 'Used';
463 return `${count} ${action}`;
464 };
465
466 const mostUsedDisplay = mostUsedFeature ?
467 mostUsedFeature.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()).replace(/\bSeo\b/g, 'SEO') :
468 'None';
469 const mostUsedSubtitle = getFeatureSubtitle(mostUsedFeature, mostUsedCount);
470
471 return {
472 // Row 1: Core Performance Metrics
473 postsAnalyzed: {
474 value: state.metrics.content_optimized || 0,
475 change: state.metrics.content_optimized_change || 0,
476 label: 'Posts Analyzed'
477 },
478 aiOptimizations: {
479 value: state.metrics.ai_actions || 0,
480 change: 0, // No change tracking for total actions yet
481 label: 'AI Optimizations'
482 },
483 averageSeoScore: {
484 value: state.metrics.average_seo_score || 0,
485 change: state.metrics.seo_score_change || 0,
486 label: 'Average SEO Score',
487 suffix: '/100'
488 },
489 totalCost: {
490 value: state.metrics.total_cost || 0,
491 change: state.metrics.cost_change || 0,
492 label: 'AI Credits Used',
493 prefix: '$',
494 subtitle: `${state.metrics.total_tokens || 0} tokens`
495 },
496 // Row 2: Feature & Efficiency Metrics
497 featuresUsed: {
498 value: state.metrics.features_used_count || 0,
499 change: 0, // No change tracking for features count yet
500 label: 'Features Used'
501 },
502 timeSaved: {
503 value: Math.round((state.metrics.time_saved || 0) / 60),
504 change: state.metrics.time_saved_change || 0,
505 label: 'Time Saved',
506 suffix: 'h',
507 subtitle: `${state.metrics.ai_actions || 0} AI actions`
508 },
509 mostUsedFeature: {
510 value: mostUsedDisplay,
511 change: 0, // No change tracking for most used feature yet
512 label: 'Most Used Feature',
513 subtitle: mostUsedSubtitle,
514 isText: true
515 },
516 successRate: {
517 value: state.metrics.success_rate || 0,
518 change: 0, // No change tracking for success rate yet
519 label: 'Success Rate',
520 suffix: '%'
521 }
522 };
523 },
524 };
525
526 // Create and export store
527 const store = createReduxStore('thinkrank/analytics', {
528 reducer,
529 actions,
530 selectors,
531 });
532
533 export default store;
534