Add campaigns component and services

This commit is contained in:
2024-12-27 01:32:48 -05:00
parent aaa95a5b9e
commit e8b5f8ce07
13 changed files with 883 additions and 24 deletions

View File

@@ -0,0 +1,237 @@
import fetch from 'node-fetch';
import { TimeManager } from '../utils/time.utils.js';
import { RedisService } from './redis.service.js';
const METRIC_IDS = {
PLACED_ORDER: 'Y8cqcF'
};
export class ReportingService {
constructor(apiKey, apiRevision) {
this.apiKey = apiKey;
this.apiRevision = apiRevision;
this.baseUrl = 'https://a.klaviyo.com/api';
this.timeManager = new TimeManager();
this.redisService = new RedisService();
this._pendingReportRequest = null;
}
async getCampaignReports(params = {}) {
try {
// Check if there's a pending request
if (this._pendingReportRequest) {
console.log('[ReportingService] Using pending campaign report request');
return this._pendingReportRequest;
}
// Try to get from cache first
const cacheKey = this.redisService._getCacheKey('campaign_reports', params);
let cachedData = null;
try {
cachedData = await this.redisService.get(`${cacheKey}:raw`);
if (cachedData) {
console.log('[ReportingService] Using cached campaign report data');
return cachedData;
}
} catch (cacheError) {
console.warn('[ReportingService] Cache error:', cacheError);
}
// Create new request promise
this._pendingReportRequest = (async () => {
console.log('[ReportingService] Fetching fresh campaign report data');
const range = this.timeManager.getDateRange(params.timeRange || 'last30days');
const payload = {
data: {
type: "campaign-values-report",
attributes: {
timeframe: {
start: range.start.toISO(),
end: range.end.toISO()
},
statistics: [
"delivery_rate",
"delivered",
"recipients",
"open_rate",
"opens_unique",
"opens",
"click_rate",
"clicks_unique",
"click_to_open_rate",
"conversion_value",
"conversion_uniques"
],
conversion_metric_id: METRIC_IDS.PLACED_ORDER,
filter: 'equals(send_channel,"email")'
}
}
};
const response = await fetch(`${this.baseUrl}/campaign-values-reports`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Klaviyo-API-Key ${this.apiKey}`,
'revision': this.apiRevision
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
console.error('[ReportingService] API Error:', errorData);
throw new Error(`Klaviyo API error: ${response.status} ${response.statusText}`);
}
const reportData = await response.json();
// Cache the response for 10 minutes
try {
await this.redisService.set(`${cacheKey}:raw`, reportData, 600);
} catch (cacheError) {
console.warn('[ReportingService] Cache set error:', cacheError);
}
return reportData;
})();
const result = await this._pendingReportRequest;
this._pendingReportRequest = null;
return result;
} catch (error) {
console.error('[ReportingService] Error fetching campaign reports:', error);
this._pendingReportRequest = null;
throw error;
}
}
async getCampaignDetails(campaignIds = []) {
try {
if (!Array.isArray(campaignIds) || campaignIds.length === 0) {
return [];
}
// Process in batches of 5 to avoid rate limits
const batchSize = 5;
const campaignDetails = [];
for (let i = 0; i < campaignIds.length; i += batchSize) {
const batch = campaignIds.slice(i, i + batchSize);
const batchPromises = batch.map(async (campaignId) => {
try {
// Try to get from cache first
const cacheKey = this.redisService._getCacheKey('campaign_details', { campaignId });
const cachedData = await this.redisService.get(`${cacheKey}:raw`);
if (cachedData) {
return cachedData;
}
const response = await fetch(
`${this.baseUrl}/campaigns/${campaignId}?include=campaign-messages`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Klaviyo-API-Key ${this.apiKey}`,
'revision': this.apiRevision
}
}
);
if (!response.ok) {
console.error(`Error fetching campaign ${campaignId}:`, response.statusText);
return null;
}
const campaignData = await response.json();
// Cache individual campaign data for 1 hour
await this.redisService.set(`${cacheKey}:raw`, campaignData, 3600);
return campaignData;
} catch (error) {
console.error(`Error processing campaign ${campaignId}:`, error);
return null;
}
});
const batchResults = await Promise.all(batchPromises);
campaignDetails.push(...batchResults.filter(Boolean));
// Add delay between batches if not the last batch
if (i + batchSize < campaignIds.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return campaignDetails;
} catch (error) {
console.error('[ReportingService] Error fetching campaign details:', error);
throw error;
}
}
async getEnrichedCampaignReports(params = {}) {
try {
// Get campaign reports first
const reportData = await this.getCampaignReports(params);
// Extract campaign IDs from the report
const campaignIds = reportData.data?.attributes?.results?.map(
result => result.groupings?.campaign_id
).filter(Boolean) || [];
// Get campaign details including messages
const campaignDetails = await this.getCampaignDetails(campaignIds);
// Merge report data with campaign details
const enrichedData = reportData.data?.attributes?.results?.map(report => {
const campaignId = report.groupings?.campaign_id;
const campaignDetail = campaignDetails.find(
detail => detail.data?.id === campaignId
);
const campaignMessages = campaignDetail?.included?.filter(
item => item.type === 'campaign-message'
) || [];
return {
id: campaignId,
name: campaignDetail?.data?.attributes?.name || "Unnamed Campaign",
subject: campaignMessages[0]?.attributes?.content?.subject || "",
send_time: report.groupings?.send_time,
stats: {
delivery_rate: report.statistics?.delivery_rate || 0,
delivered: report.statistics?.delivered || 0,
recipients: report.statistics?.recipients || 0,
open_rate: report.statistics?.open_rate || 0,
opens_unique: report.statistics?.opens_unique || 0,
opens: report.statistics?.opens || 0,
click_rate: report.statistics?.click_rate || 0,
clicks_unique: report.statistics?.clicks_unique || 0,
click_to_open_rate: report.statistics?.click_to_open_rate || 0,
conversion_value: report.statistics?.conversion_value || 0,
conversion_uniques: report.statistics?.conversion_uniques || 0
}
};
}).filter(Boolean) || [];
return {
data: enrichedData,
meta: {
total_count: enrichedData.length
}
};
} catch (error) {
console.error('[ReportingService] Error getting enriched campaign reports:', error);
throw error;
}
}
}