Add in subjects from messages correctly
This commit is contained in:
@@ -1,18 +1,16 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { createEventsRouter } from './events.routes.js';
|
import { createEventsRouter } from './events.routes.js';
|
||||||
|
import { createMetricsRoutes } from './metrics.routes.js';
|
||||||
import { createCampaignsRouter } from './campaigns.routes.js';
|
import { createCampaignsRouter } from './campaigns.routes.js';
|
||||||
import { createReportingRouter } from './reporting.routes.js';
|
import { createReportingRouter } from './reporting.routes.js';
|
||||||
|
|
||||||
export function createApiRouter(apiKey, apiRevision) {
|
export function createApiRouter(apiKey, apiRevision) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Mount events routes
|
// Mount routers
|
||||||
router.use('/events', createEventsRouter(apiKey, apiRevision));
|
router.use('/events', createEventsRouter(apiKey, apiRevision));
|
||||||
|
router.use('/metrics', createMetricsRoutes(apiKey, apiRevision));
|
||||||
// Mount campaigns routes
|
|
||||||
router.use('/campaigns', createCampaignsRouter(apiKey, apiRevision));
|
router.use('/campaigns', createCampaignsRouter(apiKey, apiRevision));
|
||||||
|
|
||||||
// Mount reporting routes
|
|
||||||
router.use('/reporting', createReportingRouter(apiKey, apiRevision));
|
router.use('/reporting', createReportingRouter(apiKey, apiRevision));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -4,31 +4,52 @@ import { TimeManager } from '../utils/time.utils.js';
|
|||||||
|
|
||||||
export function createReportingRouter(apiKey, apiRevision) {
|
export function createReportingRouter(apiKey, apiRevision) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const timeManager = new TimeManager();
|
|
||||||
const reportingService = new ReportingService(apiKey, apiRevision);
|
const reportingService = new ReportingService(apiKey, apiRevision);
|
||||||
|
const timeManager = new TimeManager();
|
||||||
|
|
||||||
// Get campaign reports with optional filtering
|
// Get campaign reports by time range
|
||||||
router.get('/campaigns', async (req, res) => {
|
router.get('/campaigns/:timeRange', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { timeRange, startDate, endDate } = req.query;
|
const { timeRange } = req.params;
|
||||||
|
const { startDate, endDate } = req.query;
|
||||||
|
|
||||||
let params = {};
|
let params = {};
|
||||||
if (timeRange === 'custom') {
|
if (timeRange === 'custom' && startDate && endDate) {
|
||||||
if (!startDate || !endDate) {
|
const range = timeManager.getCustomRange(startDate, endDate);
|
||||||
return res.status(400).json({ error: 'Custom range requires startDate and 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 {
|
} else {
|
||||||
params = { timeRange: timeRange || 'last30days' };
|
params = { timeRange };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Reporting Route] Fetching campaign reports with params:', params);
|
const data = await reportingService.getCampaignReports(params);
|
||||||
const data = await reportingService.getEnrichedCampaignReports(params);
|
|
||||||
console.log('[Reporting Route] Success:', {
|
|
||||||
count: data.data?.length || 0
|
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('[Reporting Route] Error:', error);
|
console.error('[Reporting Route] Error:', error);
|
||||||
res.status(500).json({
|
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;
|
return router;
|
||||||
}
|
}
|
||||||
@@ -88,15 +88,65 @@ export class ReportingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reportData = await response.json();
|
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 {
|
try {
|
||||||
await this.redisService.set(`${cacheKey}:raw`, reportData, 600);
|
await this.redisService.set(`${cacheKey}:raw`, enrichedData, 600);
|
||||||
} catch (cacheError) {
|
} catch (cacheError) {
|
||||||
console.warn('[ReportingService] Cache set error:', cacheError);
|
console.warn('[ReportingService] Cache set error:', cacheError);
|
||||||
}
|
}
|
||||||
|
|
||||||
return reportData;
|
return enrichedData;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const result = await this._pendingReportRequest;
|
const result = await this._pendingReportRequest;
|
||||||
@@ -111,127 +161,47 @@ export class ReportingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCampaignDetails(campaignIds = []) {
|
async getCampaignDetails(campaignIds = []) {
|
||||||
try {
|
if (!Array.isArray(campaignIds) || campaignIds.length === 0) {
|
||||||
if (!Array.isArray(campaignIds) || campaignIds.length === 0) {
|
return [];
|
||||||
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 = {}) {
|
const campaignDetails = await Promise.all(
|
||||||
try {
|
campaignIds.map(async (campaignId) => {
|
||||||
// Get campaign reports first
|
const response = await fetch(
|
||||||
const reportData = await this.getCampaignReports(params);
|
`${this.baseUrl}/campaigns/${campaignId}?include=campaign-messages`,
|
||||||
|
{
|
||||||
// Extract campaign IDs from the report
|
headers: {
|
||||||
const campaignIds = reportData.data?.attributes?.results?.map(
|
'Accept': 'application/json',
|
||||||
result => result.groupings?.campaign_id
|
'Authorization': `Klaviyo-API-Key ${this.apiKey}`,
|
||||||
).filter(Boolean) || [];
|
'revision': this.apiRevision
|
||||||
|
}
|
||||||
// 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(
|
if (!response.ok) {
|
||||||
item => item.type === 'campaign-message'
|
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 {
|
return {
|
||||||
id: campaignId,
|
id: data.data.id,
|
||||||
name: campaignDetail?.data?.attributes?.name || "Unnamed Campaign",
|
type: data.data.type,
|
||||||
subject: campaignMessages[0]?.attributes?.content?.subject || "",
|
attributes: {
|
||||||
send_time: report.groupings?.send_time,
|
...data.data.attributes,
|
||||||
stats: {
|
name: data.data.attributes.name,
|
||||||
delivery_rate: report.statistics?.delivery_rate || 0,
|
send_time: data.data.attributes.send_time,
|
||||||
delivered: report.statistics?.delivered || 0,
|
subject: message?.attributes?.content?.subject
|
||||||
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 {
|
return campaignDetails;
|
||||||
data: enrichedData,
|
|
||||||
meta: {
|
|
||||||
total_count: enrichedData.length
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[ReportingService] Error getting enriched campaign reports:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user