Spaces:
Sleeping
Sleeping
File size: 5,314 Bytes
9a43362 d9ba1d6 9a43362 d9ba1d6 9a43362 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | 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<EngagementMetrics | null> {
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<EngagementMetrics | null> {
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<EngagementMetrics> {
// 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<CarouselAnalytics[]> {
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 [];
}
}
|