Add in subjects from messages correctly

This commit is contained in:
2024-12-27 11:09:22 -05:00
parent e8b5f8ce07
commit 09cc3e2e5c
3 changed files with 126 additions and 166 deletions

View File

@@ -1,18 +1,16 @@
import express from 'express';
import { createEventsRouter } from './events.routes.js';
import { createMetricsRoutes } from './metrics.routes.js';
import { createCampaignsRouter } from './campaigns.routes.js';
import { createReportingRouter } from './reporting.routes.js';
export function createApiRouter(apiKey, apiRevision) {
const router = express.Router();
// Mount events routes
// Mount routers
router.use('/events', createEventsRouter(apiKey, apiRevision));
// Mount campaigns routes
router.use('/metrics', createMetricsRoutes(apiKey, apiRevision));
router.use('/campaigns', createCampaignsRouter(apiKey, apiRevision));
// Mount reporting routes
router.use('/reporting', createReportingRouter(apiKey, apiRevision));
return router;

View File

@@ -4,31 +4,52 @@ import { TimeManager } from '../utils/time.utils.js';
export function createReportingRouter(apiKey, apiRevision) {
const router = express.Router();
const timeManager = new TimeManager();
const reportingService = new ReportingService(apiKey, apiRevision);
const timeManager = new TimeManager();
// Get campaign reports with optional filtering
router.get('/campaigns', async (req, res) => {
// Get campaign reports by time range
router.get('/campaigns/:timeRange', async (req, res) => {
try {
const { timeRange, startDate, endDate } = req.query;
const { timeRange } = req.params;
const { startDate, endDate } = req.query;
let params = {};
if (timeRange === 'custom') {
if (!startDate || !endDate) {
return res.status(400).json({ error: 'Custom range requires startDate and endDate' });
if (timeRange === 'custom' && startDate && endDate) {
const range = timeManager.getCustomRange(startDate, endDate);
if (!range) {
return res.status(400).json({ error: 'Invalid date range' });
}
params = { startDate, endDate };
params = { startDate: range.start.toISO(), endDate: range.end.toISO() };
} else {
params = { timeRange: timeRange || 'last30days' };
params = { timeRange };
}
console.log('[Reporting Route] Fetching campaign reports with params:', params);
const data = await reportingService.getEnrichedCampaignReports(params);
console.log('[Reporting Route] Success:', {
count: data.data?.length || 0
});
const data = await reportingService.getCampaignReports(params);
res.json(data);
// Transform the data to match the expected format
const transformedData = {
data: data.data.map(campaign => ({
id: campaign.id,
name: campaign.name,
subject: campaign.subject,
send_time: campaign.send_time,
stats: {
delivery_rate: campaign.attributes?.statistics?.delivery_rate || 0,
delivered: campaign.attributes?.statistics?.delivered || 0,
recipients: campaign.attributes?.statistics?.recipients || 0,
open_rate: campaign.attributes?.statistics?.open_rate || 0,
opens_unique: campaign.attributes?.statistics?.opens_unique || 0,
opens: campaign.attributes?.statistics?.opens || 0,
click_rate: campaign.attributes?.statistics?.click_rate || 0,
clicks_unique: campaign.attributes?.statistics?.clicks_unique || 0,
click_to_open_rate: campaign.attributes?.statistics?.click_to_open_rate || 0,
conversion_value: campaign.attributes?.statistics?.conversion_value || 0,
conversion_uniques: campaign.attributes?.statistics?.conversion_uniques || 0
}
}))
};
res.json(transformedData);
} catch (error) {
console.error('[Reporting Route] Error:', error);
res.status(500).json({
@@ -39,34 +60,5 @@ export function createReportingRouter(apiKey, apiRevision) {
}
});
// Get campaign reports by time range
router.get('/campaigns/:timeRange', async (req, res) => {
try {
const { timeRange } = req.params;
let result;
if (timeRange === 'custom') {
const { startDate, endDate } = req.query;
if (!startDate || !endDate) {
return res.status(400).json({ error: 'Custom range requires startDate and endDate' });
}
result = await reportingService.getEnrichedCampaignReports({
startDate,
endDate
});
} else {
result = await reportingService.getEnrichedCampaignReports({
timeRange
});
}
res.json(result);
} catch (error) {
console.error("[Reporting Route] Error:", error);
res.status(500).json({ error: error.message });
}
});
return router;
}

View File

@@ -88,15 +88,65 @@ export class ReportingService {
}
const reportData = await response.json();
console.log('[ReportingService] Raw report data:', JSON.stringify(reportData, null, 2));
// Cache the response for 10 minutes
// 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`, reportData, 600);
await this.redisService.set(`${cacheKey}:raw`, enrichedData, 600);
} catch (cacheError) {
console.warn('[ReportingService] Cache set error:', cacheError);
}
return reportData;
return enrichedData;
})();
const result = await this._pendingReportRequest;
@@ -111,127 +161,47 @@ export class ReportingService {
}
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;
if (!Array.isArray(campaignIds) || campaignIds.length === 0) {
return [];
}
}
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 campaignDetails = await Promise.all(
campaignIds.map(async (campaignId) => {
const response = await fetch(
`${this.baseUrl}/campaigns/${campaignId}?include=campaign-messages`,
{
headers: {
'Accept': 'application/json',
'Authorization': `Klaviyo-API-Key ${this.apiKey}`,
'revision': this.apiRevision
}
}
);
const campaignMessages = campaignDetail?.included?.filter(
item => item.type === 'campaign-message'
) || [];
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: 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
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
}
};
}).filter(Boolean) || [];
})
);
return {
data: enrichedData,
meta: {
total_count: enrichedData.length
}
};
} catch (error) {
console.error('[ReportingService] Error getting enriched campaign reports:', error);
throw error;
}
return campaignDetails;
}
}