Bing Analytics and Insights added, background jobs added, database setup updated, environment setup updated, frontend updated, backend updated.
Onboarding Manager and Router Manager refactored, analytics and background jobs added, database setup updated, environment setup updated, frontend updated, backend updated. Critical onboarding database migration implemented.
This commit is contained in:
225
frontend/src/api/analytics.ts
Normal file
225
frontend/src/api/analytics.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Analytics API Service
|
||||
*
|
||||
* Handles communication with the backend analytics endpoints for retrieving
|
||||
* platform analytics data from connected services like GSC, Wix, and WordPress.
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
// Types
|
||||
export interface AnalyticsMetrics {
|
||||
total_clicks?: number;
|
||||
total_impressions?: number;
|
||||
avg_ctr?: number;
|
||||
avg_position?: number;
|
||||
total_queries?: number;
|
||||
top_queries?: Array<{
|
||||
query: string;
|
||||
clicks: number;
|
||||
impressions: number;
|
||||
ctr: number;
|
||||
position: number;
|
||||
}>;
|
||||
top_pages?: Array<{
|
||||
page: string;
|
||||
clicks: number;
|
||||
impressions: number;
|
||||
ctr: number;
|
||||
}>;
|
||||
// Additional properties for Bing analytics
|
||||
connection_status?: string;
|
||||
connected_sites?: number;
|
||||
sites?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
Url?: string; // Bing API uses uppercase Url
|
||||
status?: string;
|
||||
[key: string]: any; // Allow additional properties
|
||||
}>;
|
||||
connected_since?: string;
|
||||
scope?: string;
|
||||
insights?: any;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface PlatformAnalytics {
|
||||
platform: string;
|
||||
metrics: AnalyticsMetrics;
|
||||
date_range: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
last_updated: string;
|
||||
status: 'success' | 'error' | 'partial';
|
||||
error_message?: string;
|
||||
// Additional properties that may be present in analytics data
|
||||
connection_status?: string;
|
||||
sites?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
Url?: string; // Bing API uses uppercase Url
|
||||
status?: string;
|
||||
[key: string]: any; // Allow additional properties
|
||||
}>;
|
||||
connected_sites?: number;
|
||||
connected_since?: string;
|
||||
scope?: string;
|
||||
insights?: any;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface AnalyticsSummary {
|
||||
total_platforms: number;
|
||||
connected_platforms: number;
|
||||
successful_data: number;
|
||||
total_clicks: number;
|
||||
total_impressions: number;
|
||||
overall_ctr: number;
|
||||
platforms: Record<string, {
|
||||
status: string;
|
||||
last_updated: string;
|
||||
metrics_count?: number;
|
||||
error?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AnalyticsResponse {
|
||||
success: boolean;
|
||||
data: Record<string, PlatformAnalytics>;
|
||||
summary: AnalyticsSummary;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PlatformConnectionStatus {
|
||||
connected: boolean;
|
||||
sites_count: number;
|
||||
sites: Array<{
|
||||
siteUrl?: string;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
}>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PlatformStatusResponse {
|
||||
success: boolean;
|
||||
platforms: Record<string, PlatformConnectionStatus>;
|
||||
total_connected: number;
|
||||
}
|
||||
|
||||
class AnalyticsAPI {
|
||||
private baseUrl = '/api/analytics';
|
||||
|
||||
/**
|
||||
* Get connection status for all platforms
|
||||
*/
|
||||
async getPlatformStatus(): Promise<PlatformStatusResponse> {
|
||||
try {
|
||||
const response = await apiClient.get(`${this.baseUrl}/platforms`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Analytics API: Error getting platform status:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics data from connected platforms
|
||||
*/
|
||||
async getAnalyticsData(platforms?: string[]): Promise<AnalyticsResponse> {
|
||||
try {
|
||||
let url = `${this.baseUrl}/data`;
|
||||
|
||||
if (platforms && platforms.length > 0) {
|
||||
const platformsParam = platforms.join(',');
|
||||
url += `?platforms=${encodeURIComponent(platformsParam)}`;
|
||||
}
|
||||
|
||||
const response = await apiClient.get(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Analytics API: Error getting analytics data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics data using POST method
|
||||
*/
|
||||
async getAnalyticsDataPost(platforms?: string[]): Promise<AnalyticsResponse> {
|
||||
try {
|
||||
const response = await apiClient.post(`${this.baseUrl}/data`, {
|
||||
platforms,
|
||||
date_range: null // Could be extended to support custom date ranges
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Analytics API: Error getting analytics data (POST):', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Google Search Console analytics specifically
|
||||
*/
|
||||
async getGSCAnalytics(): Promise<PlatformAnalytics> {
|
||||
try {
|
||||
const response = await apiClient.get(`${this.baseUrl}/gsc`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Analytics API: Error getting GSC analytics:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics summary across all platforms
|
||||
*/
|
||||
async getAnalyticsSummary(): Promise<{
|
||||
success: boolean;
|
||||
summary: AnalyticsSummary;
|
||||
platforms_connected: number;
|
||||
platforms_total: number;
|
||||
}> {
|
||||
try {
|
||||
const response = await apiClient.get(`${this.baseUrl}/summary`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Analytics API: Error getting analytics summary:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test endpoint - Get platform status without authentication
|
||||
*/
|
||||
async getTestPlatformStatus(): Promise<PlatformStatusResponse> {
|
||||
try {
|
||||
const response = await apiClient.get(`${this.baseUrl}/test/status`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Analytics API: Error getting test platform status:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test endpoint - Get mock analytics data without authentication
|
||||
*/
|
||||
async getTestAnalyticsData(): Promise<AnalyticsResponse> {
|
||||
try {
|
||||
const response = await apiClient.get(`${this.baseUrl}/test/data`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Analytics API: Error getting test analytics data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const analyticsAPI = new AnalyticsAPI();
|
||||
export default analyticsAPI;
|
||||
93
frontend/src/api/bingOAuth.ts
Normal file
93
frontend/src/api/bingOAuth.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Bing Webmaster OAuth API Client
|
||||
* Handles Bing Webmaster Tools OAuth2 authentication flow
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
export interface BingOAuthStatus {
|
||||
connected: boolean;
|
||||
sites: Array<{
|
||||
id: number;
|
||||
access_token: string;
|
||||
scope: string;
|
||||
created_at: string;
|
||||
sites: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
status: string;
|
||||
}>;
|
||||
}>;
|
||||
total_sites: number;
|
||||
}
|
||||
|
||||
export interface BingOAuthResponse {
|
||||
auth_url: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface BingCallbackResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
access_token?: string;
|
||||
expires_in?: number;
|
||||
}
|
||||
|
||||
class BingOAuthAPI {
|
||||
/**
|
||||
* Get Bing Webmaster OAuth authorization URL
|
||||
*/
|
||||
async getAuthUrl(): Promise<BingOAuthResponse> {
|
||||
try {
|
||||
console.log('BingOAuthAPI: Making GET request to /bing/auth/url');
|
||||
const response = await apiClient.get('/bing/auth/url');
|
||||
console.log('BingOAuthAPI: Response received:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('BingOAuthAPI: Error getting Bing OAuth URL:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Bing Webmaster connection status
|
||||
*/
|
||||
async getStatus(): Promise<BingOAuthStatus> {
|
||||
try {
|
||||
const response = await apiClient.get('/bing/status');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error getting Bing OAuth status:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect a Bing Webmaster site
|
||||
*/
|
||||
async disconnectSite(tokenId: number): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.delete(`/bing/disconnect/${tokenId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting Bing site:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check for Bing OAuth service
|
||||
*/
|
||||
async healthCheck(): Promise<{ status: string; service: string; timestamp: string; version: string }> {
|
||||
try {
|
||||
const response = await apiClient.get('/bing/health');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error checking Bing OAuth health:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const bingOAuthAPI = new BingOAuthAPI();
|
||||
221
frontend/src/api/cachedAnalytics.ts
Normal file
221
frontend/src/api/cachedAnalytics.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Cached Analytics API Client
|
||||
*
|
||||
* Wraps the analytics API with intelligent caching to reduce redundant requests
|
||||
* and improve performance while managing cache invalidation.
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import analyticsCache from '../services/analyticsCache';
|
||||
|
||||
interface PlatformAnalytics {
|
||||
platform: string;
|
||||
metrics: Record<string, any>;
|
||||
date_range: { start: string; end: string };
|
||||
last_updated: string;
|
||||
status: string;
|
||||
error_message?: string;
|
||||
}
|
||||
|
||||
interface AnalyticsSummary {
|
||||
total_platforms: number;
|
||||
connected_platforms: number;
|
||||
successful_data: number;
|
||||
total_clicks: number;
|
||||
total_impressions: number;
|
||||
overall_ctr: number;
|
||||
platforms: Record<string, any>;
|
||||
}
|
||||
|
||||
interface PlatformConnectionStatus {
|
||||
connected: boolean;
|
||||
sites_count: number;
|
||||
sites: any[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface AnalyticsResponse {
|
||||
data: Record<string, PlatformAnalytics>;
|
||||
summary: AnalyticsSummary;
|
||||
status: Record<string, PlatformConnectionStatus>;
|
||||
}
|
||||
|
||||
class CachedAnalyticsAPI {
|
||||
private readonly CACHE_TTL = {
|
||||
PLATFORM_STATUS: 30 * 60 * 1000, // 30 minutes - status changes rarely
|
||||
ANALYTICS_DATA: 60 * 60 * 1000, // 60 minutes - analytics data cached for 1 hour
|
||||
USER_SITES: 120 * 60 * 1000, // 120 minutes - user sites change very rarely
|
||||
};
|
||||
|
||||
/**
|
||||
* Get platform connection status with caching
|
||||
*/
|
||||
async getPlatformStatus(): Promise<{ platforms: Record<string, PlatformConnectionStatus> }> {
|
||||
const endpoint = '/api/analytics/platforms';
|
||||
|
||||
// Try to get from cache first
|
||||
const cached = analyticsCache.get<{ platforms: Record<string, PlatformConnectionStatus> }>(endpoint);
|
||||
if (cached) {
|
||||
console.log('📦 Analytics Cache HIT: Platform status (cached for 30 minutes)');
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
console.log('🌐 Analytics API: Fetching platform status... (will cache for 30 minutes)');
|
||||
const response = await apiClient.get(endpoint);
|
||||
|
||||
// Cache the result with extended TTL
|
||||
analyticsCache.set(endpoint, undefined, response.data, this.CACHE_TTL.PLATFORM_STATUS);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics data with caching
|
||||
*/
|
||||
async getAnalyticsData(platforms?: string[], bypassCache: boolean = false): Promise<AnalyticsResponse> {
|
||||
const params = platforms ? { platforms: platforms.join(',') } : undefined;
|
||||
const endpoint = '/api/analytics/data';
|
||||
|
||||
// If bypassing cache, add timestamp to force fresh request
|
||||
const requestParams = bypassCache ? { ...params, _t: Date.now() } : params;
|
||||
|
||||
// Try to get from cache first (unless bypassing)
|
||||
if (!bypassCache) {
|
||||
const cached = analyticsCache.get<AnalyticsResponse>(endpoint, params);
|
||||
if (cached) {
|
||||
console.log('📦 Analytics Cache HIT: Analytics data (cached for 60 minutes)');
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
console.log('🌐 Analytics API: Fetching analytics data... (will cache for 60 minutes)', requestParams);
|
||||
const response = await apiClient.get(endpoint, { params: requestParams });
|
||||
|
||||
// Cache the result with extended TTL (unless bypassing)
|
||||
if (!bypassCache) {
|
||||
analyticsCache.set(endpoint, params, response.data, this.CACHE_TTL.ANALYTICS_DATA);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate platform status cache
|
||||
*/
|
||||
invalidatePlatformStatus(): void {
|
||||
analyticsCache.invalidate('/api/analytics/platforms');
|
||||
console.log('🔄 Analytics Cache: Platform status invalidated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate analytics data cache
|
||||
*/
|
||||
invalidateAnalyticsData(): void {
|
||||
analyticsCache.invalidate('/api/analytics/data');
|
||||
console.log('🔄 Analytics Cache: Analytics data invalidated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all analytics cache
|
||||
*/
|
||||
invalidateAll(): void {
|
||||
analyticsCache.invalidate('analytics');
|
||||
console.log('🔄 Analytics Cache: All analytics cache invalidated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh analytics data (bypass cache)
|
||||
*/
|
||||
async forceRefreshAnalyticsData(platforms?: string[]): Promise<AnalyticsResponse> {
|
||||
// Try to clear backend cache first (but don't fail if it doesn't work)
|
||||
try {
|
||||
await this.clearBackendCache(platforms);
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Backend cache clearing failed, continuing with frontend cache clear:', error);
|
||||
}
|
||||
|
||||
// Always invalidate frontend cache
|
||||
this.invalidateAnalyticsData();
|
||||
|
||||
// Finally get fresh data with cache bypass
|
||||
return this.getAnalyticsData(platforms, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear backend analytics cache
|
||||
*/
|
||||
async clearBackendCache(platforms?: string[]): Promise<void> {
|
||||
try {
|
||||
if (platforms && platforms.length > 0) {
|
||||
// Clear cache for specific platforms
|
||||
for (const platform of platforms) {
|
||||
await apiClient.post('/api/analytics/cache/clear', null, {
|
||||
params: { platform }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Clear all cache
|
||||
await apiClient.post('/api/analytics/cache/clear');
|
||||
}
|
||||
console.log('🔄 Backend analytics cache cleared');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to clear backend cache:', error);
|
||||
// Don't throw error, just log it - frontend cache clearing is more important
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh platform status (bypass cache)
|
||||
*/
|
||||
async forceRefreshPlatformStatus(): Promise<{ platforms: Record<string, PlatformConnectionStatus> }> {
|
||||
this.invalidatePlatformStatus();
|
||||
return this.getPlatformStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics for debugging
|
||||
*/
|
||||
getCacheStats() {
|
||||
return analyticsCache.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
*/
|
||||
clearCache(): void {
|
||||
analyticsCache.invalidate();
|
||||
console.log('🗑️ Analytics Cache: All cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics data with database-first caching (most aggressive)
|
||||
* Use this when you know the data is stored in the database
|
||||
*/
|
||||
async getAnalyticsDataFromDB(platforms?: string[]): Promise<AnalyticsResponse> {
|
||||
const params = platforms ? { platforms: platforms.join(',') } : undefined;
|
||||
const endpoint = '/api/analytics/data';
|
||||
|
||||
// Try to get from cache first
|
||||
const cached = analyticsCache.get<AnalyticsResponse>(endpoint, params);
|
||||
if (cached) {
|
||||
console.log('📦 Analytics Cache HIT: Analytics data from DB (cached for 2 hours)');
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
console.log('🌐 Analytics API: Fetching analytics data from DB... (will cache for 2 hours)', params);
|
||||
const response = await apiClient.get(endpoint, { params });
|
||||
|
||||
// Cache the result with database TTL (very long since it's from DB)
|
||||
analyticsCache.setDatabaseData(endpoint, params, response.data);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const cachedAnalyticsAPI = new CachedAnalyticsAPI();
|
||||
|
||||
export default cachedAnalyticsAPI;
|
||||
Reference in New Issue
Block a user