diff --git a/dashboard-server/klaviyo-server/routes/index.js b/dashboard-server/klaviyo-server/routes/index.js index fe84e92..c83c323 100644 --- a/dashboard-server/klaviyo-server/routes/index.js +++ b/dashboard-server/klaviyo-server/routes/index.js @@ -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; diff --git a/dashboard-server/klaviyo-server/routes/reporting.routes.js b/dashboard-server/klaviyo-server/routes/reporting.routes.js index a7768a7..2e1b2da 100644 --- a/dashboard-server/klaviyo-server/routes/reporting.routes.js +++ b/dashboard-server/klaviyo-server/routes/reporting.routes.js @@ -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; } \ No newline at end of file diff --git a/dashboard-server/klaviyo-server/services/reporting.service.js b/dashboard-server/klaviyo-server/services/reporting.service.js index a7bb023..5be3761 100644 --- a/dashboard-server/klaviyo-server/services/reporting.service.js +++ b/dashboard-server/klaviyo-server/services/reporting.service.js @@ -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; } } \ No newline at end of file