Integrating Google Analytics with Google Ads
Learn how to effectively integrate Google Analytics with Google Ads for better tracking and optimization of your campaigns.
Published: 2023-11-20
By: Michael Mares
Maximize your Google Ads ROI with this automated budget allocation script that dynamically shifts budget from underperforming to high-performing campaigns.
Managing budgets across multiple Google Ads campaigns can be challenging. High-performing campaigns may exhaust their budget early, while underperforming campaigns continue to spend. This script solves that problem by automatically reallocating budget based on performance metrics.
This script analyzes your campaign performance over a specified time period, calculates a performance score for each campaign based on conversion metrics, and then redistributes your budget to favor the best-performing campaigns.
// Configuration
var CONFIG = {
// Look back period in days for performance analysis
lookBackDays: 14,
// Minimum number of conversions required for a campaign to be considered
minConversions: 3,
// Minimum number of clicks required for a campaign to be considered
minClicks: 50,
// Maximum budget increase allowed (as a percentage)
maxBudgetIncrease: 50,
// Maximum budget decrease allowed (as a percentage)
maxBudgetDecrease: 30,
// Minimum budget (in account currency) - campaigns won't go below this
minBudget: 10,
// Campaigns to exclude (exact match)
excludeCampaigns: ['Brand Campaign', 'Remarketing Campaign'],
// Only include campaigns with these labels (leave empty for all campaigns)
campaignLabels: ['Budget Optimization'],
// Performance metric weights (must sum to 1.0)
weights: {
conversionRate: 0.3,
costPerConversion: 0.4,
clickThroughRate: 0.1,
impressionShare: 0.2
},
// Email notification settings
sendEmail: true,
emailAddresses: 'your.email@example.com'
};
function main() {
// Validate configuration
validateConfig();
// Get date range for analysis
var dateRange = getDateRange();
// Get eligible campaigns
var campaigns = getEligibleCampaigns();
if (campaigns.length === 0) {
Logger.log('No eligible campaigns found. Exiting script.');
return;
}
// Get performance data for all campaigns
var campaignData = getCampaignPerformanceData(campaigns, dateRange);
// Calculate performance scores
calculatePerformanceScores(campaignData);
// Allocate budgets based on performance scores
var budgetChanges = allocateBudgets(campaignData);
// Apply new budgets
applyBudgetChanges(budgetChanges);
// Send email report if enabled
if (CONFIG.sendEmail && budgetChanges.length > 0) {
sendEmailReport(budgetChanges, campaignData);
}
Logger.log('Script completed. Made ' + budgetChanges.length + ' budget adjustments.');
}
function validateConfig() {
// Check that weights sum to 1.0
var weightSum = Object.values(CONFIG.weights).reduce((a, b) => a + b, 0);
if (Math.abs(weightSum - 1.0) > 0.01) {
throw new Error('Performance metric weights must sum to 1.0. Current sum: ' + weightSum);
}
}
function getDateRange() {
var today = new Date();
var startDate = new Date(today.getTime() - (CONFIG.lookBackDays _ 24 _ 60 _ 60 _ 1000));
return {
startDate: Utilities.formatDate(startDate, 'UTC', 'yyyyMMdd'),
endDate: Utilities.formatDate(today, 'UTC', 'yyyyMMdd')
};
}
function getEligibleCampaigns() {
var campaignSelector = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.withCondition('Impressions > 0');
// Filter by labels if specified
if (CONFIG.campaignLabels.length > 0) {
campaignSelector = campaignSelector.withCondition("LabelNames CONTAINS_ANY ['" +
CONFIG.campaignLabels.join("','") + "']");
}
var campaigns = [];
var iterator = campaignSelector.get();
while (iterator.hasNext()) {
var campaign = iterator.next();
var campaignName = campaign.getName();
// Skip excluded campaigns
if (CONFIG.excludeCampaigns.indexOf(campaignName) !== -1) {
continue;
}
campaigns.push({
campaign: campaign,
name: campaignName,
id: campaign.getId(),
currentBudget: campaign.getBudget().getAmount()
});
}
return campaigns;
}
function getCampaignPerformanceData(campaigns, dateRange) {
var campaignIds = campaigns.map(c => c.id);
// Build report query
var report = AdsApp.report(
'SELECT CampaignId, Clicks, Impressions, Cost, Conversions, ConversionValue, SearchImpressionShare ' +
'FROM CAMPAIGN_PERFORMANCE_REPORT ' +
'WHERE CampaignId IN [' + campaignIds.join(',') + '] ' +
'AND Impressions > 0 ' +
'DURING ' + dateRange.startDate + ',' + dateRange.endDate
);
// Process report data
var campaignData = {};
campaigns.forEach(c => {
campaignData[c.id] = {
campaign: c.campaign,
name: c.name,
currentBudget: c.currentBudget,
clicks: 0,
impressions: 0,
cost: 0,
conversions: 0,
conversionValue: 0,
searchImpressionShare: 0,
performanceScore: 0
};
});
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var campaignId = row['CampaignId'];
if (campaignId in campaignData) {
var data = campaignData[campaignId];
data.clicks = parseInt(row['Clicks'], 10);
data.impressions = parseInt(row['Impressions'], 10);
data.cost = parseFloat(row['Cost'].replace(/,/g, ''));
data.conversions = parseFloat(row['Conversions']);
data.conversionValue = parseFloat(row['ConversionValue'].replace(/,/g, ''));
// Parse impression share (remove % and handle < 10% or > 90% cases)
var impressionShare = row['SearchImpressionShare'];
if (impressionShare === '--') {
data.searchImpressionShare = 0;
} else {
impressionShare = impressionShare.replace(/[<%>]/g, '').replace(/,/g, '');
data.searchImpressionShare = parseFloat(impressionShare) / 100;
}
// Calculate derived metrics
data.ctr = data.impressions > 0 ? data.clicks / data.impressions : 0;
data.convRate = data.clicks > 0 ? data.conversions / data.clicks : 0;
data.cpa = data.conversions > 0 ? data.cost / data.conversions : Infinity;
data.roas = data.cost > 0 ? data.conversionValue / data.cost : 0;
}
}
return campaignData;
}
function calculatePerformanceScores(campaignData) {
// Get all campaigns with sufficient data
var eligibleCampaigns = Object.values(campaignData).filter(data =>
data.clicks >= CONFIG.minClicks && data.conversions >= CONFIG.minConversions
);
if (eligibleCampaigns.length === 0) {
Logger.log('No campaigns with sufficient data for scoring. Exiting.');
return;
}
// Calculate min/max values for normalization
var metrics = {
convRate: { min: Infinity, max: -Infinity },
cpa: { min: Infinity, max: -Infinity },
ctr: { min: Infinity, max: -Infinity },
impressionShare: { min: Infinity, max: -Infinity }
};
eligibleCampaigns.forEach(data => {
// For CPA, lower is better, so we'll invert it later
if (data.cpa < metrics.cpa.min && data.cpa > 0) metrics.cpa.min = data.cpa;
if (data.cpa > metrics.cpa.max) metrics.cpa.max = data.cpa;
if (data.convRate < metrics.convRate.min) metrics.convRate.min = data.convRate;
if (data.convRate > metrics.convRate.max) metrics.convRate.max = data.convRate;
if (data.ctr < metrics.ctr.min) metrics.ctr.min = data.ctr;
if (data.ctr > metrics.ctr.max) metrics.ctr.max = data.ctr;
if (data.searchImpressionShare < metrics.impressionShare.min) metrics.impressionShare.min = data.searchImpressionShare;
if (data.searchImpressionShare > metrics.impressionShare.max) metrics.impressionShare.max = data.searchImpressionShare;
});
// Calculate normalized scores for each campaign
Object.values(campaignData).forEach(data => {
// Skip campaigns with insufficient data
if (data.clicks < CONFIG.minClicks || data.conversions < CONFIG.minConversions) {
data.performanceScore = 0;
return;
}
// Normalize each metric to a 0-1 scale
var normalizedScores = {};
// For CPA, lower is better, so invert the normalization
var cpaRange = metrics.cpa.max - metrics.cpa.min;
normalizedScores.costPerConversion = cpaRange > 0 ?
1 - ((data.cpa - metrics.cpa.min) / cpaRange) : 0.5;
var convRateRange = metrics.convRate.max - metrics.convRate.min;
normalizedScores.conversionRate = convRateRange > 0 ?
(data.convRate - metrics.convRate.min) / convRateRange : 0.5;
var ctrRange = metrics.ctr.max - metrics.ctr.min;
normalizedScores.clickThroughRate = ctrRange > 0 ?
(data.ctr - metrics.ctr.min) / ctrRange : 0.5;
var impressionShareRange = metrics.impressionShare.max - metrics.impressionShare.min;
normalizedScores.impressionShare = impressionShareRange > 0 ?
(data.searchImpressionShare - metrics.impressionShare.min) / impressionShareRange : 0.5;
// Calculate weighted score
data.performanceScore =
(normalizedScores.conversionRate * CONFIG.weights.conversionRate) +
(normalizedScores.costPerConversion * CONFIG.weights.costPerConversion) +
(normalizedScores.clickThroughRate * CONFIG.weights.clickThroughRate) +
(normalizedScores.impressionShare * CONFIG.weights.impressionShare);
// Store normalized scores for reporting
data.normalizedScores = normalizedScores;
});
}
function allocateBudgets(campaignData) {
var budgetChanges = [];
var totalScore = 0;
var totalCurrentBudget = 0;
// Get eligible campaigns and calculate totals
var eligibleCampaigns = Object.values(campaignData).filter(data =>
data.performanceScore > 0
);
if (eligibleCampaigns.length === 0) {
Logger.log('No eligible campaigns for budget reallocation. Exiting.');
return budgetChanges;
}
eligibleCampaigns.forEach(data => {
totalScore += data.performanceScore;
totalCurrentBudget += data.currentBudget;
});
// Calculate ideal budget for each campaign based on performance score
eligibleCampaigns.forEach(data => {
var idealShare = data.performanceScore / totalScore;
var idealBudget = totalCurrentBudget \* idealShare;
// Apply constraints
var maxIncrease = data.currentBudget * (1 + (CONFIG.maxBudgetIncrease / 100));
var maxDecrease = data.currentBudget * (1 - (CONFIG.maxBudgetDecrease / 100));
var newBudget = Math.min(idealBudget, maxIncrease);
newBudget = Math.max(newBudget, maxDecrease);
newBudget = Math.max(newBudget, CONFIG.minBudget);
// Round to 2 decimal places
newBudget = Math.round(newBudget * 100) / 100;
// Only make changes if the difference is significant
if (Math.abs(newBudget - data.currentBudget) >= 1.0) {
budgetChanges.push({
campaign: data.campaign,
name: data.name,
oldBudget: data.currentBudget,
newBudget: newBudget,
percentChange: ((newBudget - data.currentBudget) / data.currentBudget * 100).toFixed(1),
performanceScore: data.performanceScore.toFixed(2)
});
}
});
return budgetChanges;
}
function applyBudgetChanges(budgetChanges) {
budgetChanges.forEach(change => {
try {
var budget = change.campaign.getBudget();
budget.setAmount(change.newBudget);
Logger.log('Updated budget for ' + change.name + ' from ' +
change.oldBudget + ' to ' + change.newBudget);
} catch (e) {
Logger.log('Error updating budget for ' + change.name + ': ' + e.message);
}
});
}
function sendEmailReport(budgetChanges, campaignData) {
var currencySymbol = AdsApp.currentAccount().getCurrencyCode();
var subject = 'Google Ads Budget Reallocation - ' + new Date().toDateString();
var body = '<h2>Budget Reallocation Report</h2>';
body += '<p>The following budget adjustments were made based on campaign performance:</p>';
body += '<table border="1" cellpadding="3" style="border-collapse: collapse;">';
body += '<tr><th>Campaign</th><th>Old Budget</th><th>New Budget</th><th>Change</th><th>Performance Score</th></tr>';
budgetChanges.forEach(change => {
var changeClass = parseFloat(change.percentChange) >= 0 ? 'green' : 'red';
body += '<tr>';
body += '<td>' + change.name + '</td>';
body += '<td>' + currencySymbol + change.oldBudget + '</td>';
body += '<td>' + currencySymbol + change.newBudget + '</td>';
body += '<td style="color: ' + changeClass + ';">' + change.percentChange + '%</td>';
body += '<td>' + change.performanceScore + '</td>';
body += '</tr>';
});
body += '</table>';
// Add performance metrics table
body += '<h3>Campaign Performance Metrics (Last ' + CONFIG.lookBackDays + ' Days)</h3>';
body += '<table border="1" cellpadding="3" style="border-collapse: collapse;">';
body += '<tr><th>Campaign</th><th>Clicks</th><th>Conv.</th><th>Conv. Rate</th><th>CPA</th><th>Impr. Share</th></tr>';
Object.values(campaignData)
.filter(data => data.impressions > 0)
.sort((a, b) => b.performanceScore - a.performanceScore)
.forEach(data => {
body += '<tr>';
body += '<td>' + data.name + '</td>';
body += '<td>' + data.clicks + '</td>';
body += '<td>' + data.conversions.toFixed(2) + '</td>';
body += '<td>' + (data.convRate _ 100).toFixed(2) + '%</td>';
body += '<td>' + (data.cpa === Infinity ? '-' : currencySymbol + data.cpa.toFixed(2)) + '</td>';
body += '<td>' + (data.searchImpressionShare _ 100).toFixed(2) + '%</td>';
body += '</tr>';
});
body += '</table>';
MailApp.sendEmail({
to: CONFIG.emailAddresses,
subject: subject,
htmlBody: body
});
}
This script has helped my clients achieve:
Have you tried automated budget management before? What challenges did you face? Let me know in the comments!
Learn how to effectively integrate Google Analytics with Google Ads for better tracking and optimization of your campaigns.
Learn how to effectively target and segment your audience in Google Ads to improve campaign performance and ROI.
Explore advanced automation techniques for Google Ads to scale your campaigns and improve efficiency.
Learn the fundamentals of Google Ads automation and how to implement basic automation rules for better campaign management.
Maximize your Google Ads ROI with this automated budget allocation script that dynamically shifts budget from underperforming to high-performing campaigns.
Learn advanced strategies for managing your Google Ads budget effectively to maximize ROI and campaign performance.