import type { EngagementMetrics, CarouselAnalytics } from '@/types/analytics'; /** * Fetch engagement metrics from publisher APIs (Zernio/Postiz). * Gracefully falls back to mock data if APIs are unavailable. */ async function fetchFromZernio(postId: string): Promise { try { const apiKey = process.env.ZERNIO_API_KEY; if (!apiKey) { console.warn('[analytics] ZERNIO_API_KEY not set, using fallback'); return null; } const response = await fetch(`https://api.zernio.com/posts/${postId}/metrics`, { headers: { Authorization: `Bearer ${apiKey}` }, }); if (!response.ok) { console.error('[analytics] Zernio API error:', response.status); return null; } const data = await response.json(); return { likes: data.likes || 0, comments: data.comments || 0, shares: data.shares || 0, saves: data.saves || 0, impressions: data.impressions || 0, reach: data.reach || 0, engagement_rate: ((data.likes + data.comments + data.shares) / data.impressions) * 100 || 0, }; } catch (error) { console.error('[analytics] fetchFromZernio failed:', error); return null; } } async function fetchFromPostiz(postId: string): Promise { try { const apiKey = process.env.POSTIZ_API_KEY; if (!apiKey) { console.warn('[analytics] POSTIZ_API_KEY not set, using fallback'); return null; } const response = await fetch(`https://api.postiz.com/posts/${postId}/metrics`, { headers: { 'X-API-Key': apiKey }, }); if (!response.ok) { console.error('[analytics] Postiz API error:', response.status); return null; } const data = await response.json(); return { likes: data.likeCount || 0, comments: data.commentCount || 0, shares: data.shareCount || 0, saves: data.saveCount || 0, impressions: data.impressionCount || 0, reach: data.reachCount || 0, engagement_rate: ((data.likeCount + data.commentCount + data.shareCount) / data.impressionCount) * 100 || 0, }; } catch (error) { console.error('[analytics] fetchFromPostiz failed:', error); return null; } } function generateMockMetrics(): EngagementMetrics { // Realistic mock data for testing without real APIs const impressions = Math.floor(Math.random() * 5000) + 500; const reach = Math.floor(impressions * 0.7); const likes = Math.floor(impressions * 0.08); const comments = Math.floor(impressions * 0.02); const shares = Math.floor(impressions * 0.01); const saves = Math.floor(impressions * 0.03); return { likes, comments, shares, saves, impressions, reach, engagement_rate: ((likes + comments + shares) / impressions) * 100, }; } /** * Fetch engagement metrics for a carousel post. * Tries Zernio first, then Postiz, then returns mock data. */ export async function getEngagementMetrics( carouselId: string, platform: 'instagram' | 'threads' | 'linkedin', externalPostId?: string ): Promise { // If we have an external post ID from publisher, try real APIs if (externalPostId) { // Try Zernio first (primary publisher) const zernioMetrics = await fetchFromZernio(externalPostId); if (zernioMetrics) return zernioMetrics; // Try Postiz as fallback const postizMetrics = await fetchFromPostiz(externalPostId); if (postizMetrics) return postizMetrics; } // Fall back to mock data (Phase 11: real DB integration in future phases) console.warn(`[analytics] No real metrics for carousel ${carouselId}, using mock data`); return generateMockMetrics(); } export async function getRecentCarouselAnalytics( limit: number = 10 ): Promise { try { // Import dynamically so it doesn't break analytics if sqlite is missing in pure web enviroments const { getScheduledCarousels } = require('@/lib/scheduler/scheduler'); const published = await getScheduledCarousels('published'); if (!published || published.length === 0) { return []; } const carouselsWithMetrics: CarouselAnalytics[] = await Promise.all( published.slice(0, limit).map(async (carousel: any) => { // Grab the first platform from their platforms array, defaulting to instagram const platform = (carousel.platforms && carousel.platforms.length > 0) ? carousel.platforms[0] : 'instagram'; return { carousel_id: carousel.carouselId, title: carousel.title || 'Untitled Post', platform: platform as 'instagram' | 'threads' | 'linkedin', published_at: carousel.publishedAt || carousel.scheduledFor, slide_count: 5, // Default fallback since scheduler doesn't currently store this context template_type: 'bold-statement', // Default fallback palette: 'blue', // Default fallback metrics: await getEngagementMetrics(carousel.carouselId, platform), }; }) ); return carouselsWithMetrics.sort( (a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime() ); } catch (error) { console.error('[analytics] getRecentCarouselAnalytics failed:', error); return []; } }