Skip to main content
Glama

Facebook Ads Management Control Panel

by codprocess
recommendationService.js19.9 kB
/** * Recommendation Service * Generates optimization recommendations for Facebook ads */ 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'); /** * Recommendation Service class */ class RecommendationService { /** * Create a new Recommendation 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); } } /** * Get budget optimization recommendations * @param {string} adAccountId - Ad account ID * @returns {Promise<Array>} - Budget recommendations */ async getBudgetRecommendations(adAccountId) { try { // Get all campaigns for the ad account const campaigns = await Campaign.find({ adAccountId, userId: this.userId, status: 'ACTIVE' }); if (campaigns.length === 0) { return []; } const recommendations = []; // Analyze each campaign for (const campaign of campaigns) { // Get campaign analytics for the last 30 days const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const analytics = await Analytics.find({ entityId: campaign.campaignId, entityType: 'campaign', userId: this.userId, date: { $gte: thirtyDaysAgo } }).sort({ date: 1 }); if (analytics.length === 0) { continue; } // Calculate performance metrics const performance = this._calculatePerformanceMetrics(analytics); // Generate budget recommendations const budgetRecs = this._generateBudgetRecommendations(campaign, performance); if (budgetRecs.length > 0) { recommendations.push({ campaignId: campaign.campaignId, campaignName: campaign.name, performance, recommendations: budgetRecs }); } } return recommendations; } catch (error) { logger.error(`Error generating budget recommendations: ${error.message}`); throw error; } } /** * Get targeting recommendations * @param {string} adSetId - Ad set ID * @returns {Promise<Object>} - Targeting recommendations */ async getTargetingRecommendations(adSetId) { try { // Get ad set const adSet = await AdSet.findOne({ adSetId, userId: this.userId }); if (!adSet) { throw new Error(`Ad set not found: ${adSetId}`); } // Get ad set analytics for the last 30 days const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const analytics = await Analytics.find({ entityId: adSetId, entityType: 'adset', userId: this.userId, date: { $gte: thirtyDaysAgo } }).sort({ date: 1 }); // Calculate performance metrics const performance = this._calculatePerformanceMetrics(analytics); // Generate targeting recommendations const targetingRecs = this._generateTargetingRecommendations(adSet, performance); return { adSetId: adSet.adSetId, adSetName: adSet.name, performance, currentTargeting: adSet.targeting, recommendations: targetingRecs }; } catch (error) { logger.error(`Error generating targeting recommendations: ${error.message}`); throw error; } } /** * Get creative performance recommendations * @param {string} adAccountId - Ad account ID * @returns {Promise<Array>} - Creative recommendations */ async getCreativeRecommendations(adAccountId) { try { // Get all active ads for the ad account const ads = await Ad.find({ userId: this.userId, status: 'ACTIVE' }).populate({ path: 'adSetId', match: { adAccountId } }); if (ads.length === 0) { return []; } const recommendations = []; // Analyze each ad for (const ad of ads) { // Get ad analytics for the last 30 days const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const analytics = await Analytics.find({ entityId: ad.adId, entityType: 'ad', userId: this.userId, date: { $gte: thirtyDaysAgo } }).sort({ date: 1 }); if (analytics.length === 0) { continue; } // Calculate performance metrics const performance = this._calculatePerformanceMetrics(analytics); // Generate creative recommendations const creativeRecs = this._generateCreativeRecommendations(ad, performance); if (creativeRecs.length > 0) { recommendations.push({ adId: ad.adId, adName: ad.name, performance, creativeType: ad.creative.type, recommendations: creativeRecs }); } } return recommendations; } catch (error) { logger.error(`Error generating creative recommendations: ${error.message}`); throw error; } } /** * Calculate performance metrics from analytics data * @param {Array} analytics - Analytics data * @returns {Object} - Performance metrics * @private */ _calculatePerformanceMetrics(analytics) { if (analytics.length === 0) { return { impressions: 0, clicks: 0, ctr: 0, cpc: 0, spend: 0, conversions: 0, conversionRate: 0, costPerConversion: 0, roas: 0, trend: 'stable' }; } // Calculate totals const totals = { impressions: 0, clicks: 0, spend: 0, conversions: 0 }; analytics.forEach(data => { totals.impressions += data.impressions; totals.clicks += data.clicks; totals.spend += data.spend; totals.conversions += data.conversions; }); // Calculate averages const metrics = { impressions: totals.impressions, clicks: totals.clicks, ctr: totals.clicks / totals.impressions * 100 || 0, cpc: totals.spend / totals.clicks || 0, spend: totals.spend, conversions: totals.conversions, conversionRate: totals.conversions / totals.clicks * 100 || 0, costPerConversion: totals.spend / totals.conversions || 0, roas: totals.conversions * 10 / totals.spend || 0 // Assuming $10 per conversion }; // Calculate trend if (analytics.length >= 7) { const recentData = analytics.slice(-7); const olderData = analytics.slice(-14, -7); const recentConversions = recentData.reduce((sum, data) => sum + data.conversions, 0); const olderConversions = olderData.reduce((sum, data) => sum + data.conversions, 0); if (recentConversions > olderConversions * 1.2) { metrics.trend = 'improving'; } else if (recentConversions < olderConversions * 0.8) { metrics.trend = 'declining'; } else { metrics.trend = 'stable'; } } else { metrics.trend = 'insufficient_data'; } return metrics; } /** * Generate budget recommendations for a campaign * @param {Object} campaign - Campaign object * @param {Object} performance - Performance metrics * @returns {Array} - Budget recommendations * @private */ _generateBudgetRecommendations(campaign, performance) { const recommendations = []; // Check if campaign has sufficient data if (performance.impressions < 1000 || performance.clicks < 10) { recommendations.push({ type: 'info', message: 'Insufficient data to make budget recommendations', reason: 'Campaign needs more data to generate accurate recommendations', action: 'Continue running the campaign to gather more data' }); return recommendations; } // Check if campaign is performing well if (performance.roas >= 2) { // High ROAS, recommend increasing budget const currentBudget = campaign.dailyBudget || campaign.lifetimeBudget / 30; const recommendedIncrease = Math.min(currentBudget * 0.2, 100); recommendations.push({ type: 'increase_budget', message: `Increase daily budget by $${recommendedIncrease.toFixed(2)}`, reason: `Campaign is performing well with a ROAS of ${performance.roas.toFixed(2)}`, action: 'Gradually increase budget to scale successful campaign', data: { currentBudget: currentBudget, recommendedBudget: currentBudget + recommendedIncrease, increase: recommendedIncrease, increasePercentage: 20 } }); } else if (performance.roas < 1) { // Low ROAS, recommend decreasing budget const currentBudget = campaign.dailyBudget || campaign.lifetimeBudget / 30; const recommendedDecrease = currentBudget * 0.3; recommendations.push({ type: 'decrease_budget', message: `Decrease daily budget by $${recommendedDecrease.toFixed(2)}`, reason: `Campaign has a low ROAS of ${performance.roas.toFixed(2)}`, action: 'Reduce spend on underperforming campaign while optimizing targeting and creative', data: { currentBudget: currentBudget, recommendedBudget: currentBudget - recommendedDecrease, decrease: recommendedDecrease, decreasePercentage: 30 } }); } // Check cost per conversion if (performance.costPerConversion > 0) { const targetCPA = 10; // Example target cost per acquisition if (performance.costPerConversion > targetCPA * 1.5) { recommendations.push({ type: 'high_cpa', message: `Cost per conversion ($${performance.costPerConversion.toFixed(2)}) is significantly higher than target ($${targetCPA.toFixed(2)})`, reason: 'Campaign is acquiring customers at a high cost', action: 'Review targeting and creative to improve conversion efficiency' }); } } // Check if campaign budget is being spent if (performance.spend < campaign.dailyBudget * 0.8 && performance.trend !== 'improving') { recommendations.push({ type: 'underspending', message: 'Campaign is consistently underspending its budget', reason: 'Targeting may be too narrow or bid strategy may need adjustment', action: 'Broaden targeting or adjust bid strategy to increase delivery' }); } return recommendations; } /** * Generate targeting recommendations for an ad set * @param {Object} adSet - Ad set object * @param {Object} performance - Performance metrics * @returns {Array} - Targeting recommendations * @private */ _generateTargetingRecommendations(adSet, performance) { const recommendations = []; const targeting = adSet.targeting || {}; // Check if ad set has sufficient data if (performance.impressions < 1000 || performance.clicks < 10) { recommendations.push({ type: 'info', message: 'Insufficient data to make targeting recommendations', reason: 'Ad set needs more data to generate accurate recommendations', action: 'Continue running the ad set to gather more data' }); return recommendations; } // Check audience size if (targeting.audience_size && targeting.audience_size < 100000) { recommendations.push({ type: 'audience_size', message: 'Audience size may be too small', reason: 'Small audiences can limit delivery and increase costs', action: 'Consider broadening age range, adding more interests, or expanding locations', data: { currentSize: targeting.audience_size, recommendedMinimum: 100000 } }); } // Check age targeting if (targeting.age_min && targeting.age_max) { const ageRange = targeting.age_max - targeting.age_min; if (ageRange < 10 && performance.ctr < 1) { recommendations.push({ type: 'age_targeting', message: 'Age range is narrow with low CTR', reason: 'Narrow age targeting may be limiting performance', action: 'Consider expanding age range to reach more potential customers', data: { currentAgeMin: targeting.age_min, currentAgeMax: targeting.age_max, recommendedAgeMin: Math.max(13, targeting.age_min - 5), recommendedAgeMax: Math.min(65, targeting.age_max + 5) } }); } } // Check detailed targeting (interests) if (targeting.flexible_spec && targeting.flexible_spec.length > 3) { recommendations.push({ type: 'interest_targeting', message: 'Too many interest layers may be restricting audience', reason: 'Multiple interest layers create a smaller, more specific audience', action: 'Simplify interest targeting to broaden reach while maintaining relevance', data: { currentInterests: targeting.flexible_spec.length, recommendedMaximum: 3 } }); } // Check placement optimization if (targeting.placements && targeting.placements.length < 3) { recommendations.push({ type: 'placements', message: 'Limited placements may be restricting delivery', reason: 'Using more placements can improve reach and reduce costs', action: 'Consider enabling automatic placements to optimize delivery', data: { currentPlacements: targeting.placements, recommendation: 'automatic_placements' } }); } // Check device targeting if (targeting.device_platforms && targeting.device_platforms.length < 2) { recommendations.push({ type: 'device_targeting', message: 'Limited device targeting may be restricting audience', reason: 'Targeting only specific devices limits potential reach', action: 'Consider enabling all device types to maximize audience', data: { currentDevices: targeting.device_platforms, recommendedDevices: ['mobile', 'desktop'] } }); } // Check location targeting if (targeting.geo_locations && Object.keys(targeting.geo_locations).length === 1) { recommendations.push({ type: 'location_targeting', message: 'Limited location targeting may be restricting audience', reason: 'Targeting only specific locations limits potential reach', action: 'Consider expanding location targeting to similar areas', data: { currentLocations: targeting.geo_locations } }); } return recommendations; } /** * Generate creative recommendations for an ad * @param {Object} ad - Ad object * @param {Object} performance - Performance metrics * @returns {Array} - Creative recommendations * @private */ _generateCreativeRecommendations(ad, performance) { const recommendations = []; const creative = ad.creative || {}; // Check if ad has sufficient data if (performance.impressions < 1000 || performance.clicks < 10) { recommendations.push({ type: 'info', message: 'Insufficient data to make creative recommendations', reason: 'Ad needs more data to generate accurate recommendations', action: 'Continue running the ad to gather more data' }); return recommendations; } // Check CTR if (performance.ctr < 1) { recommendations.push({ type: 'low_ctr', message: 'Ad has a low click-through rate', reason: 'Creative may not be engaging enough for the target audience', action: 'Test new headlines, images, or calls to action to improve engagement', data: { currentCTR: performance.ctr, benchmarkCTR: 1.0 } }); // Specific recommendations based on creative type if (creative.type === 'IMAGE') { recommendations.push({ type: 'image_creative', message: 'Consider testing different image creative', reason: 'Current image may not be resonating with audience', action: 'Test images with people, bright colors, or product demonstrations' }); } else if (creative.type === 'VIDEO') { recommendations.push({ type: 'video_creative', message: 'Video may not be capturing attention quickly', reason: 'First 3 seconds of video are critical for engagement', action: 'Test videos that showcase value proposition in the first 3 seconds' }); } } // Check conversion rate if (performance.conversionRate < 2) { recommendations.push({ type: 'low_conversion_rate', message: 'Ad has a low conversion rate', reason: 'Creative may be attracting clicks but not driving conversions', action: 'Ensure ad creative sets clear expectations that match landing page', data: { currentConversionRate: performance.conversionRate, benchmarkConversionRate: 2.0 } }); } // Check headline and body text if (creative.title && creative.title.length > 40) { recommendations.push({ type: 'headline_length', message: 'Headline may be too long', reason: 'Shorter headlines often perform better', action: 'Test shorter, more impactful headlines (25-40 characters)', data: { currentLength: creative.title.length, recommendedMaxLength: 40 } }); } if (creative.body && creative.body.length > 125) { recommendations.push({ type: 'body_length', message: 'Ad copy may be too long', reason: 'Shorter ad copy often performs better on Facebook', action: 'Test more concise ad copy (80-125 characters)', data: { currentLength: creative.body.length, recommendedMaxLength: 125 } }); } // Check call to action if (!creative.callToAction) { recommendations.push({ type: 'missing_cta', message: 'Ad is missing a clear call to action', reason: 'CTAs guide users on what action to take next', action: 'Add a clear call to action button like "Learn More" or "Shop Now"' }); } // Check for A/B testing recommendations.push({ type: 'ab_testing', message: 'Consider A/B testing multiple ad variations', reason: 'Testing different creatives helps identify what resonates with your audience', action: 'Create 3-5 variations of this ad with different images, headlines, or CTAs' }); return recommendations; } } /** * Create a Recommendation Service instance for a user * @param {Object} user - User object * @returns {RecommendationService} - Recommendation Service instance */ const createForUser = (user) => { return new RecommendationService(user); }; module.exports = { RecommendationService, createForUser };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/codprocess/facebook-ads-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server