237 lines
8.3 KiB
JavaScript
237 lines
8.3 KiB
JavaScript
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();
|
|
console.log('[ReportingService] Raw report data:', JSON.stringify(reportData, null, 2));
|
|
|
|
// Get campaign IDs from the report
|
|
const campaignIds = reportData.data?.attributes?.results?.map(result =>
|
|
result.groupings?.campaign_id
|
|
).filter(Boolean) || [];
|
|
|
|
console.log('[ReportingService] Extracted campaign IDs:', campaignIds);
|
|
|
|
// Get campaign details including send time and subject lines
|
|
const campaignDetails = await this.getCampaignDetails(campaignIds);
|
|
console.log('[ReportingService] Campaign details:', JSON.stringify(campaignDetails, null, 2));
|
|
|
|
// Merge campaign details with report data
|
|
const enrichedData = {
|
|
data: reportData.data.attributes.results.map(result => {
|
|
const campaignId = result.groupings.campaign_id;
|
|
const details = campaignDetails.find(detail => detail.id === campaignId);
|
|
const message = details?.included?.find(item => item.type === 'campaign-message');
|
|
|
|
return {
|
|
id: campaignId,
|
|
name: details.attributes.name,
|
|
subject: details.attributes.subject,
|
|
send_time: details.attributes.send_time,
|
|
attributes: {
|
|
statistics: {
|
|
delivery_rate: result.statistics.delivery_rate,
|
|
delivered: result.statistics.delivered,
|
|
recipients: result.statistics.recipients,
|
|
open_rate: result.statistics.open_rate,
|
|
opens_unique: result.statistics.opens_unique,
|
|
opens: result.statistics.opens,
|
|
click_rate: result.statistics.click_rate,
|
|
clicks_unique: result.statistics.clicks_unique,
|
|
click_to_open_rate: result.statistics.click_to_open_rate,
|
|
conversion_value: result.statistics.conversion_value,
|
|
conversion_uniques: result.statistics.conversion_uniques
|
|
}
|
|
}
|
|
};
|
|
})
|
|
.sort((a, b) => {
|
|
const dateA = new Date(a.send_time);
|
|
const dateB = new Date(b.send_time);
|
|
return dateB - dateA; // Sort by date descending
|
|
})
|
|
};
|
|
|
|
console.log('[ReportingService] Enriched data:', JSON.stringify(enrichedData, null, 2));
|
|
|
|
// Cache the enriched response for 10 minutes
|
|
try {
|
|
await this.redisService.set(`${cacheKey}:raw`, enrichedData, 600);
|
|
} catch (cacheError) {
|
|
console.warn('[ReportingService] Cache set error:', cacheError);
|
|
}
|
|
|
|
return enrichedData;
|
|
})();
|
|
|
|
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 = []) {
|
|
if (!Array.isArray(campaignIds) || campaignIds.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const fetchWithTimeout = async (campaignId, retries = 3) => {
|
|
for (let i = 0; i < retries; i++) {
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
|
|
|
const response = await fetch(
|
|
`${this.baseUrl}/campaigns/${campaignId}?include=campaign-messages`,
|
|
{
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Authorization': `Klaviyo-API-Key ${this.apiKey}`,
|
|
'revision': this.apiRevision
|
|
},
|
|
signal: controller.signal
|
|
}
|
|
);
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch campaign ${campaignId}: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (!data.data) {
|
|
throw new Error(`Invalid response for campaign ${campaignId}`);
|
|
}
|
|
|
|
const message = data.included?.find(item => item.type === 'campaign-message');
|
|
|
|
return {
|
|
id: data.data.id,
|
|
type: data.data.type,
|
|
attributes: {
|
|
...data.data.attributes,
|
|
name: data.data.attributes.name,
|
|
send_time: data.data.attributes.send_time,
|
|
subject: message?.attributes?.content?.subject
|
|
}
|
|
};
|
|
} catch (error) {
|
|
if (i === retries - 1) throw error;
|
|
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); // Exponential backoff
|
|
}
|
|
}
|
|
};
|
|
|
|
// Process in smaller chunks to avoid overwhelming the API
|
|
const chunkSize = 10;
|
|
const campaignDetails = [];
|
|
|
|
for (let i = 0; i < campaignIds.length; i += chunkSize) {
|
|
const chunk = campaignIds.slice(i, i + chunkSize);
|
|
const results = await Promise.all(
|
|
chunk.map(id => fetchWithTimeout(id).catch(error => {
|
|
console.error(`Failed to fetch campaign ${id}:`, error);
|
|
return null;
|
|
}))
|
|
);
|
|
campaignDetails.push(...results.filter(Boolean));
|
|
|
|
if (i + chunkSize < campaignIds.length) {
|
|
await new Promise(resolve => setTimeout(resolve, 1000)); // 1 second delay between chunks
|
|
}
|
|
}
|
|
|
|
return campaignDetails;
|
|
}
|
|
}
|