analyticsService.js•17.4 kB
/**
* Analytics Service
* Handles analytics data processing and insights generation
*/
const Analytics = require('../models/analytics');
const Campaign = require('../models/campaign');
const AdSet = require('../models/adSet');
const Ad = require('../models/ad');
const facebookApiService = require('./facebookApiService');
const logger = require('../utils/logger');
/**
* Analytics Service class
*/
class AnalyticsService {
/**
* Create a new Analytics Service instance
* @param {Object} user - User object
*/
constructor(user) {
this.user = user;
this.userId = user._id;
}
/**
* Initialize Facebook API service
* @returns {Promise<void>}
* @private
*/
async _initFacebookApi() {
if (!this.fbApi) {
this.fbApi = await facebookApiService.createForUser(this.user);
}
}
/**
* Fetch and store analytics data for a campaign
* @param {string} campaignId - Campaign ID
* @param {string} timeRange - Time range (e.g., 'today', 'yesterday', 'last_7_days')
* @returns {Promise<Object>} - Analytics data
*/
async fetchCampaignAnalytics(campaignId, timeRange = 'last_7_days') {
try {
await this._initFacebookApi();
// Fetch insights from Facebook
const insights = await this.fbApi.getInsights(campaignId, timeRange, [], {
level: 'campaign',
time_increment: 1 // Daily breakdown
});
// Process and store insights
const analyticsData = [];
for (const insight of insights) {
const date = new Date(insight.date_start);
// Format metrics
const metrics = {
impressions: parseInt(insight.impressions || 0),
reach: parseInt(insight.reach || 0),
clicks: parseInt(insight.clicks || 0),
ctr: parseFloat(insight.ctr || 0),
cpc: parseFloat(insight.cpc || 0),
cpm: parseFloat(insight.cpm || 0),
spend: parseFloat(insight.spend || 0),
frequency: parseFloat(insight.frequency || 0),
conversions: parseInt(insight.conversions || 0),
costPerConversion: parseFloat(insight.cost_per_conversion || 0),
conversionRate: parseFloat((insight.conversions / insight.clicks) || 0),
roas: parseFloat(insight.roas || 0)
};
// Store analytics data
const analytics = await Analytics.findOrCreate(
campaignId,
'campaign',
this.userId,
date,
metrics
);
analyticsData.push(analytics);
}
// Update campaign metrics with latest data
if (analyticsData.length > 0) {
const latestData = analyticsData[analyticsData.length - 1];
const campaign = await Campaign.findOne({ campaignId });
if (campaign) {
campaign.metrics = {
impressions: latestData.impressions,
clicks: latestData.clicks,
spend: latestData.spend,
conversions: latestData.conversions,
ctr: latestData.ctr,
cpc: latestData.cpc,
cpm: latestData.cpm,
reach: latestData.reach,
frequency: latestData.frequency,
costPerConversion: latestData.costPerConversion,
conversionRate: latestData.conversionRate,
roas: latestData.roas
};
await campaign.save();
}
}
return analyticsData;
} catch (error) {
logger.error(`Error fetching campaign analytics: ${error.message}`);
throw error;
}
}
/**
* Fetch and store analytics data for an ad set
* @param {string} adSetId - Ad set ID
* @param {string} timeRange - Time range (e.g., 'today', 'yesterday', 'last_7_days')
* @returns {Promise<Object>} - Analytics data
*/
async fetchAdSetAnalytics(adSetId, timeRange = 'last_7_days') {
try {
await this._initFacebookApi();
// Fetch insights from Facebook
const insights = await this.fbApi.getInsights(adSetId, timeRange, [], {
level: 'adset',
time_increment: 1 // Daily breakdown
});
// Process and store insights
const analyticsData = [];
for (const insight of insights) {
const date = new Date(insight.date_start);
// Format metrics
const metrics = {
impressions: parseInt(insight.impressions || 0),
reach: parseInt(insight.reach || 0),
clicks: parseInt(insight.clicks || 0),
ctr: parseFloat(insight.ctr || 0),
cpc: parseFloat(insight.cpc || 0),
cpm: parseFloat(insight.cpm || 0),
spend: parseFloat(insight.spend || 0),
frequency: parseFloat(insight.frequency || 0),
conversions: parseInt(insight.conversions || 0),
costPerConversion: parseFloat(insight.cost_per_conversion || 0),
conversionRate: parseFloat((insight.conversions / insight.clicks) || 0),
roas: parseFloat(insight.roas || 0)
};
// Store analytics data
const analytics = await Analytics.findOrCreate(
adSetId,
'adset',
this.userId,
date,
metrics
);
analyticsData.push(analytics);
}
// Update ad set metrics with latest data
if (analyticsData.length > 0) {
const latestData = analyticsData[analyticsData.length - 1];
const adSet = await AdSet.findOne({ adSetId });
if (adSet) {
adSet.metrics = {
impressions: latestData.impressions,
clicks: latestData.clicks,
spend: latestData.spend,
conversions: latestData.conversions,
ctr: latestData.ctr,
cpc: latestData.cpc,
cpm: latestData.cpm,
reach: latestData.reach,
frequency: latestData.frequency,
costPerConversion: latestData.costPerConversion,
conversionRate: latestData.conversionRate,
roas: latestData.roas
};
await adSet.save();
}
}
return analyticsData;
} catch (error) {
logger.error(`Error fetching ad set analytics: ${error.message}`);
throw error;
}
}
/**
* Fetch and store analytics data for an ad
* @param {string} adId - Ad ID
* @param {string} timeRange - Time range (e.g., 'today', 'yesterday', 'last_7_days')
* @returns {Promise<Object>} - Analytics data
*/
async fetchAdAnalytics(adId, timeRange = 'last_7_days') {
try {
await this._initFacebookApi();
// Fetch insights from Facebook
const insights = await this.fbApi.getInsights(adId, timeRange, [], {
level: 'ad',
time_increment: 1 // Daily breakdown
});
// Process and store insights
const analyticsData = [];
for (const insight of insights) {
const date = new Date(insight.date_start);
// Format metrics
const metrics = {
impressions: parseInt(insight.impressions || 0),
reach: parseInt(insight.reach || 0),
clicks: parseInt(insight.clicks || 0),
ctr: parseFloat(insight.ctr || 0),
cpc: parseFloat(insight.cpc || 0),
cpm: parseFloat(insight.cpm || 0),
spend: parseFloat(insight.spend || 0),
frequency: parseFloat(insight.frequency || 0),
conversions: parseInt(insight.conversions || 0),
costPerConversion: parseFloat(insight.cost_per_conversion || 0),
conversionRate: parseFloat((insight.conversions / insight.clicks) || 0),
roas: parseFloat(insight.roas || 0),
engagement: {
postEngagement: parseInt(insight.post_engagement || 0),
pageLikes: parseInt(insight.page_likes || 0),
postComments: parseInt(insight.post_comments || 0),
postShares: parseInt(insight.post_shares || 0),
linkClicks: parseInt(insight.link_clicks || 0),
videoViews: parseInt(insight.video_views || 0),
videoWatchTime: parseInt(insight.video_play_actions || 0)
}
};
// Store analytics data
const analytics = await Analytics.findOrCreate(
adId,
'ad',
this.userId,
date,
metrics
);
analyticsData.push(analytics);
}
// Update ad metrics with latest data
if (analyticsData.length > 0) {
const latestData = analyticsData[analyticsData.length - 1];
const ad = await Ad.findOne({ adId });
if (ad) {
ad.metrics = {
impressions: latestData.impressions,
clicks: latestData.clicks,
spend: latestData.spend,
conversions: latestData.conversions,
ctr: latestData.ctr,
cpc: latestData.cpc,
cpm: latestData.cpm,
reach: latestData.reach,
frequency: latestData.frequency,
costPerConversion: latestData.costPerConversion,
conversionRate: latestData.conversionRate,
roas: latestData.roas,
engagement: latestData.engagement.postEngagement,
videoViews: latestData.engagement.videoViews,
videoWatchTime: latestData.engagement.videoWatchTime
};
await ad.save();
}
}
return analyticsData;
} catch (error) {
logger.error(`Error fetching ad analytics: ${error.message}`);
throw error;
}
}
/**
* Get analytics data for a campaign
* @param {string} campaignId - Campaign ID
* @param {Date} startDate - Start date
* @param {Date} endDate - End date
* @returns {Promise<Array>} - Analytics data
*/
async getCampaignAnalytics(campaignId, startDate, endDate) {
try {
return await Analytics.findByEntityId(campaignId, startDate, endDate);
} catch (error) {
logger.error(`Error getting campaign analytics: ${error.message}`);
throw error;
}
}
/**
* Get analytics data for an ad set
* @param {string} adSetId - Ad set ID
* @param {Date} startDate - Start date
* @param {Date} endDate - End date
* @returns {Promise<Array>} - Analytics data
*/
async getAdSetAnalytics(adSetId, startDate, endDate) {
try {
return await Analytics.findByEntityId(adSetId, startDate, endDate);
} catch (error) {
logger.error(`Error getting ad set analytics: ${error.message}`);
throw error;
}
}
/**
* Get analytics data for an ad
* @param {string} adId - Ad ID
* @param {Date} startDate - Start date
* @param {Date} endDate - End date
* @returns {Promise<Array>} - Analytics data
*/
async getAdAnalytics(adId, startDate, endDate) {
try {
return await Analytics.findByEntityId(adId, startDate, endDate);
} catch (error) {
logger.error(`Error getting ad analytics: ${error.message}`);
throw error;
}
}
/**
* Get account overview analytics
* @param {Date} startDate - Start date
* @param {Date} endDate - End date
* @returns {Promise<Object>} - Account overview
*/
async getAccountOverview(startDate, endDate) {
try {
// Get all analytics data for the user
const campaignAnalytics = await Analytics.find({
userId: this.userId,
entityType: 'campaign',
date: { $gte: startDate, $lte: endDate }
});
// Calculate totals
const overview = {
impressions: 0,
reach: 0,
clicks: 0,
spend: 0,
conversions: 0,
ctr: 0,
cpc: 0,
cpm: 0,
costPerConversion: 0,
conversionRate: 0,
roas: 0,
campaigns: {
total: 0,
active: 0
},
adSets: {
total: 0,
active: 0
},
ads: {
total: 0,
active: 0
},
performance: {
daily: []
}
};
// Process campaign analytics
const dailyData = {};
campaignAnalytics.forEach(analytics => {
// Add to totals
overview.impressions += analytics.impressions;
overview.reach += analytics.reach;
overview.clicks += analytics.clicks;
overview.spend += analytics.spend;
overview.conversions += analytics.conversions;
// Track daily data
const dateStr = analytics.date.toISOString().split('T')[0];
if (!dailyData[dateStr]) {
dailyData[dateStr] = {
date: dateStr,
impressions: 0,
clicks: 0,
spend: 0,
conversions: 0
};
}
dailyData[dateStr].impressions += analytics.impressions;
dailyData[dateStr].clicks += analytics.clicks;
dailyData[dateStr].spend += analytics.spend;
dailyData[dateStr].conversions += analytics.conversions;
});
// Calculate averages
if (campaignAnalytics.length > 0) {
overview.ctr = overview.clicks / overview.impressions * 100;
overview.cpc = overview.spend / overview.clicks;
overview.cpm = overview.spend / overview.impressions * 1000;
overview.costPerConversion = overview.spend / overview.conversions;
overview.conversionRate = overview.conversions / overview.clicks * 100;
overview.roas = overview.conversions * 10 / overview.spend; // Assuming $10 per conversion
}
// Get campaign, ad set, and ad counts
overview.campaigns.total = await Campaign.countDocuments({ userId: this.userId });
overview.campaigns.active = await Campaign.countDocuments({ userId: this.userId, status: 'ACTIVE' });
overview.adSets.total = await AdSet.countDocuments({ userId: this.userId });
overview.adSets.active = await AdSet.countDocuments({ userId: this.userId, status: 'ACTIVE' });
overview.ads.total = await Ad.countDocuments({ userId: this.userId });
overview.ads.active = await Ad.countDocuments({ userId: this.userId, status: 'ACTIVE' });
// Format daily data
overview.performance.daily = Object.values(dailyData).sort((a, b) => a.date.localeCompare(b.date));
return overview;
} catch (error) {
logger.error(`Error getting account overview: ${error.message}`);
throw error;
}
}
/**
* Get performance comparison
* @param {string} entityId - Entity ID
* @param {string} entityType - Entity type (campaign, adset, ad)
* @param {Date} currentStartDate - Current period start date
* @param {Date} currentEndDate - Current period end date
* @param {Date} previousStartDate - Previous period start date
* @param {Date} previousEndDate - Previous period end date
* @returns {Promise<Object>} - Performance comparison
*/
async getPerformanceComparison(entityId, entityType, currentStartDate, currentEndDate, previousStartDate, previousEndDate) {
try {
// Get current period data
const currentPeriod = await Analytics.calculateAggregateMetrics(entityId, currentStartDate, currentEndDate);
// Get previous period data
const previousPeriod = await Analytics.calculateAggregateMetrics(entityId, previousStartDate, previousEndDate);
// Calculate changes
const comparison = {
current: currentPeriod || {
impressions: 0,
reach: 0,
clicks: 0,
spend: 0,
conversions: 0,
ctr: 0,
cpc: 0,
cpm: 0,
frequency: 0,
conversionRate: 0,
costPerConversion: 0,
roas: 0
},
previous: previousPeriod || {
impressions: 0,
reach: 0,
clicks: 0,
spend: 0,
conversions: 0,
ctr: 0,
cpc: 0,
cpm: 0,
frequency: 0,
conversionRate: 0,
costPerConversion: 0,
roas: 0
},
changes: {}
};
// Calculate percentage changes
if (previousPeriod) {
for (const key in currentPeriod) {
if (key === '_id') continue;
const current = currentPeriod[key] || 0;
const previous = previousPeriod[key] || 0;
if (previous === 0) {
comparison.changes[key] = current > 0 ? 100 : 0;
} else {
comparison.changes[key] = ((current - previous) / previous) * 100;
}
}
} else {
// No previous data, set all changes to 100%
for (const key in currentPeriod) {
if (key === '_id') continue;
comparison.changes[key] = currentPeriod[key] > 0 ? 100 : 0;
}
}
return comparison;
} catch (error) {
logger.error(`Error getting performance comparison: ${error.message}`);
throw error;
}
}
}
/**
* Create an Analytics Service instance for a user
* @param {Object} user - User object
* @returns {AnalyticsService} - Analytics Service instance
*/
const createForUser = (user) => {
return new AnalyticsService(user);
};
module.exports = {
AnalyticsService,
createForUser
};