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-09-25
By: Michael Mares
Boost your Google Ads performance with this powerful script that automatically tests ad variations and promotes winners based on statistical significance.
Continuous ad testing is one of the most effective ways to improve your Google Ads performance. However, manually analyzing results and determining winners can be time-consuming and prone to error. This script automates the entire process by identifying statistically significant winners and pausing underperforming ads.
This script analyzes your ad performance over a specified time period, calculates statistical significance between ad variations, and automatically promotes winners by pausing underperforming ads. It uses a robust statistical approach to ensure decisions are based on reliable data.
/**
* Automated Ad Testing and Optimization Script for Google Ads
*
* This script analyzes ad performance, identifies statistically significant winners,
* and pauses underperforming ads.
*/
// Configuration
var CONFIG = {
// Look back period in days
lookBackDays: 30,
// Minimum number of impressions required for an ad to be considered
minImpressions: 1000,
// Minimum number of clicks required for an ad to be considered
minClicks: 20,
// Confidence level for statistical significance (0.9 = 90%, 0.95 = 95%, 0.99 = 99%)
confidenceLevel: 0.95,
// Primary metric to use for determining winners
// Options: 'CTR', 'ConversionRate', 'CostPerConversion'
primaryMetric: 'CTR',
// Secondary metric to use as a tiebreaker
// Options: 'CTR', 'ConversionRate', 'CostPerConversion'
secondaryMetric: 'ConversionRate',
// For CostPerConversion, lower is better; for others, higher is better
// Campaigns to exclude (exact match)
excludeCampaigns: ['Brand Campaign', 'Competitor Campaign'],
// Only include campaigns with these labels (leave empty for all campaigns)
campaignLabels: ['Ad Testing'],
// Minimum number of ads to keep active in each ad group
minAdsPerAdGroup: 2,
// Dry run mode (set to true to preview without making changes)
dryRun: true,
// Email notification settings
sendEmail: true,
emailAddresses: 'your.email@example.com'
};
function main() {
// Validate configuration
validateConfig();
// Get date range for analysis
var dateRange = getDateRange();
// Get ad groups with multiple active ads
var adGroups = getEligibleAdGroups();
if (adGroups.length === 0) {
Logger.log('No eligible ad groups found. Exiting script.');
return;
}
// Process each ad group
var results = [];
adGroups.forEach(adGroup => {
var adGroupResults = processAdGroup(adGroup, dateRange);
if (adGroupResults) {
results.push(adGroupResults);
}
});
// Send email report if enabled
if (CONFIG.sendEmail && results.length > 0) {
sendEmailReport(results);
}
Logger.log('Script completed. Processed ' + adGroups.length + ' ad groups and found ' +
results.length + ' with statistically significant winners.');
}
function validateConfig() {
// Check that primary and secondary metrics are valid
var validMetrics = ['CTR', 'ConversionRate', 'CostPerConversion'];
if (validMetrics.indexOf(CONFIG.primaryMetric) === -1) {
throw new Error('Invalid primary metric: ' + CONFIG.primaryMetric);
}
if (validMetrics.indexOf(CONFIG.secondaryMetric) === -1) {
throw new Error('Invalid secondary metric: ' + CONFIG.secondaryMetric);
}
// Check that confidence level is valid
if (CONFIG.confidenceLevel < 0.8 || CONFIG.confidenceLevel > 0.99) {
throw new Error('Confidence level should be between 0.8 and 0.99');
}
}
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 getEligibleAdGroups() {
var campaignSelector = AdsApp.campaigns()
.withCondition('Status = ENABLED');
// Filter by labels if specified
if (CONFIG.campaignLabels.length > 0) {
campaignSelector = campaignSelector.withCondition("LabelNames CONTAINS_ANY ['" +
CONFIG.campaignLabels.join("','") + "']");
}
var campaigns = [];
var campaignIterator = campaignSelector.get();
while (campaignIterator.hasNext()) {
var campaign = campaignIterator.next();
var campaignName = campaign.getName();
// Skip excluded campaigns
if (CONFIG.excludeCampaigns.indexOf(campaignName) !== -1) {
continue;
}
campaigns.push(campaign);
}
var eligibleAdGroups = [];
campaigns.forEach(campaign => {
var adGroupIterator = campaign.adGroups()
.withCondition('Status = ENABLED')
.get();
while (adGroupIterator.hasNext()) {
var adGroup = adGroupIterator.next();
// Count active ads in the ad group
var adCount = adGroup.ads()
.withCondition('Status = ENABLED')
.withCondition('Type = EXPANDED_TEXT_AD')
.get()
.totalNumEntities();
// Only include ad groups with multiple active ads
if (adCount >= 2) {
eligibleAdGroups.push({
adGroup: adGroup,
campaign: campaign,
adGroupName: adGroup.getName(),
campaignName: campaign.getName(),
adGroupId: adGroup.getId(),
campaignId: campaign.getId()
});
}
}
});
return eligibleAdGroups;
}
function processAdGroup(adGroupData, dateRange) {
var adGroup = adGroupData.adGroup;
// Get ad performance data
var adPerformance = getAdPerformance(adGroupData, dateRange);
// Filter ads with sufficient data
var eligibleAds = adPerformance.filter(ad =>
ad.impressions >= CONFIG.minImpressions && ad.clicks >= CONFIG.minClicks
);
if (eligibleAds.length < 2) {
Logger.log('Not enough eligible ads in ad group: ' + adGroupData.adGroupName);
return null;
}
// Sort ads by primary metric
sortAdsByMetric(eligibleAds, CONFIG.primaryMetric);
// Find statistically significant winners
var winners = findStatisticallySignificantWinners(eligibleAds);
if (winners.length === 0) {
Logger.log('No statistically significant winners found in ad group: ' + adGroupData.adGroupName);
return null;
}
// Determine which ads to pause
var adsToPause = determineAdsToPause(eligibleAds, winners);
// Pause losing ads
if (!CONFIG.dryRun && adsToPause.length > 0) {
pauseAds(adsToPause);
}
return {
adGroupName: adGroupData.adGroupName,
campaignName: adGroupData.campaignName,
winners: winners,
losers: adsToPause,
allAds: eligibleAds,
dryRun: CONFIG.dryRun
};
}
function getAdPerformance(adGroupData, dateRange) {
var adPerformance = [];
// Build report query
var report = AdsApp.report(
'SELECT AdGroupId, Id, HeadlinePart1, HeadlinePart2, Description, ' +
'Impressions, Clicks, Cost, Conversions, ConversionValue ' +
'FROM AD_PERFORMANCE_REPORT ' +
'WHERE AdGroupId = ' + adGroupData.adGroupId + ' ' +
'AND Status = ENABLED ' +
'AND AdType = EXPANDED_TEXT_AD ' +
'AND Impressions > 0 ' +
'DURING ' + dateRange.startDate + ',' + dateRange.endDate
);
// Process report data
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var impressions = parseInt(row['Impressions'], 10);
var clicks = parseInt(row['Clicks'], 10);
var cost = parseFloat(row['Cost'].replace(/,/g, ''));
var conversions = parseFloat(row['Conversions']);
// Calculate metrics
var ctr = impressions > 0 ? clicks / impressions * 100 : 0;
var convRate = clicks > 0 ? conversions / clicks * 100 : 0;
var cpa = conversions > 0 ? cost / conversions : Infinity;
adPerformance.push({
id: row['Id'],
headline1: row['HeadlinePart1'],
headline2: row['HeadlinePart2'],
description: row['Description'],
impressions: impressions,
clicks: clicks,
cost: cost,
conversions: conversions,
ctr: ctr,
convRate: convRate,
cpa: cpa,
ad: AdsApp.ads().withIds([row['Id']]).get().next()
});
}
return adPerformance;
}
function sortAdsByMetric(ads, metric) {
ads.sort((a, b) => {
if (metric === 'CostPerConversion') {
// For CPA, lower is better
if (a.cpa === Infinity) return 1;
if (b.cpa === Infinity) return -1;
return a.cpa - b.cpa;
} else if (metric === 'CTR') {
// For CTR, higher is better
return b.ctr - a.ctr;
} else if (metric === 'ConversionRate') {
// For conversion rate, higher is better
return b.convRate - a.convRate;
}
return 0;
});
}
function findStatisticallySignificantWinners(ads) {
var winners = [];
var bestAd = ads[0];
// Compare best ad to all others
for (var i = 1; i < ads.length; i++) {
var challenger = ads[i];
if (isStatisticallySignificant(bestAd, challenger, CONFIG.primaryMetric)) {
winners.push(bestAd);
break;
}
// If not significant on primary metric, check secondary
if (CONFIG.secondaryMetric !== CONFIG.primaryMetric) {
if (isStatisticallySignificant(bestAd, challenger, CONFIG.secondaryMetric)) {
winners.push(bestAd);
break;
}
}
}
return winners;
}
function isStatisticallySignificant(ad1, ad2, metric) {
if (metric === 'CTR') {
return isCTRSignificant(ad1, ad2);
} else if (metric === 'ConversionRate') {
return isConvRateSignificant(ad1, ad2);
} else if (metric === 'CostPerConversion') {
// For CPA, we need a different approach
// This is simplified; a more robust approach would use confidence intervals
return ad1.conversions >= 10 && ad2.conversions >= 10 &&
(ad1.cpa < ad2.cpa * 0.8 || ad1.cpa > ad2.cpa * 1.2);
}
return false;
}
function isCTRSignificant(ad1, ad2) {
// Using the z-test for comparing two proportions
var p1 = ad1.clicks / ad1.impressions;
var p2 = ad2.clicks / ad2.impressions;
var p = (ad1.clicks + ad2.clicks) / (ad1.impressions + ad2.impressions);
var z = (p1 - p2) / Math.sqrt(p * (1 - p) * (1/ad1.impressions + 1/ad2.impressions));
// Convert confidence level to z-score
var zScore = getZScore(CONFIG.confidenceLevel);
return Math.abs(z) > zScore;
}
function isConvRateSignificant(ad1, ad2) {
// Using the z-test for comparing two proportions
var p1 = ad1.conversions / ad1.clicks;
var p2 = ad2.conversions / ad2.clicks;
var p = (ad1.conversions + ad2.conversions) / (ad1.clicks + ad2.clicks);
var z = (p1 - p2) / Math.sqrt(p * (1 - p) * (1/ad1.clicks + 1/ad2.clicks));
// Convert confidence level to z-score
var zScore = getZScore(CONFIG.confidenceLevel);
return Math.abs(z) > zScore;
}
function getZScore(confidenceLevel) {
// Common z-scores for confidence levels
if (confidenceLevel >= 0.99) return 2.576;
if (confidenceLevel >= 0.98) return 2.326;
if (confidenceLevel >= 0.95) return 1.96;
if (confidenceLevel >= 0.90) return 1.645;
return 1.28; // 80% confidence
}
function determineAdsToPause(allAds, winners) {
var adsToPause = [];
// If we have winners and enough ads, we can pause the losers
if (winners.length > 0 && allAds.length > CONFIG.minAdsPerAdGroup) {
// Get winner IDs
var winnerIds = winners.map(ad => ad.id);
// Find ads to pause (all except winners and keeping minimum number)
var remainingAds = allAds.filter(ad => winnerIds.indexOf(ad.id) === -1);
// Sort remaining ads by primary metric to keep the best ones
sortAdsByMetric(remainingAds, CONFIG.primaryMetric);
// Determine how many ads to pause
var numToPause = Math.max(0, allAds.length - CONFIG.minAdsPerAdGroup);
// Get the worst performing ads to pause
adsToPause = remainingAds.slice(Math.max(0, remainingAds.length - numToPause));
}
return adsToPause;
}
function pauseAds(adsToPause) {
adsToPause.forEach(adData => {
try {
adData.ad.pause();
Logger.log('Paused ad: ' + adData.headline1 + ' - ' + adData.headline2);
} catch (e) {
Logger.log('Error pausing ad: ' + e.message);
}
});
}
function sendEmailReport(results) {
var subject = 'Google Ads Ad Testing Report - ' + new Date().toDateString();
var dryRunText = CONFIG.dryRun ? '(DRY RUN - No changes made)' : '';
var body = '<h2>Ad Testing Report ' + dryRunText + '</h2>';
body += '<p>The script analyzed ' + results.length + ' ad groups and found statistically significant winners.</p>';
results.forEach(result => {
body += '<h3>Campaign: ' + result.campaignName + ' / Ad Group: ' + result.adGroupName + '</h3>';
// Winners table
body += '<h4>Winning Ads</h4>';
body += '<table border="1" cellpadding="3" style="border-collapse: collapse;">';
body += '<tr><th>Headlines</th><th>Description</th><th>Impressions</th><th>Clicks</th>' +
'<th>CTR</th><th>Conv.</th><th>Conv. Rate</th><th>CPA</th></tr>';
result.winners.forEach(ad => {
body += '<tr style="background-color: #e6ffe6;">';
body += '<td>' + ad.headline1 + '<br>' + ad.headline2 + '</td>';
body += '<td>' + ad.description + '</td>';
body += '<td>' + ad.impressions + '</td>';
body += '<td>' + ad.clicks + '</td>';
body += '<td>' + ad.ctr.toFixed(2) + '%</td>';
body += '<td>' + ad.conversions.toFixed(2) + '</td>';
body += '<td>' + ad.convRate.toFixed(2) + '%</td>';
body += '<td>' + (ad.cpa === Infinity ? '-' : '$' + ad.cpa.toFixed(2)) + '</td>';
body += '</tr>';
});
body += '</table>';
// Losers table
if (result.losers.length > 0) {
body += '<h4>Ads to Pause</h4>';
body += '<table border="1" cellpadding="3" style="border-collapse: collapse;">';
body += '<tr><th>Headlines</th><th>Description</th><th>Impressions</th><th>Clicks</th>' +
'<th>CTR</th><th>Conv.</th><th>Conv. Rate</th><th>CPA</th></tr>';
result.losers.forEach(ad => {
body += '<tr style="background-color: #ffe6e6;">';
body += '<td>' + ad.headline1 + '<br>' + ad.headline2 + '</td>';
body += '<td>' + ad.description + '</td>';
body += '<td>' + ad.impressions + '</td>';
body += '<td>' + ad.clicks + '</td>';
body += '<td>' + ad.ctr.toFixed(2) + '%</td>';
body += '<td>' + ad.conversions.toFixed(2) + '</td>';
body += '<td>' + ad.convRate.toFixed(2) + '%</td>';
body += '<td>' + (ad.cpa === Infinity ? '-' : '$' + ad.cpa.toFixed(2)) + '</td>';
body += '</tr>';
});
body += '</table>';
}
body += '<hr>';
});
if (CONFIG.dryRun) {
body += '<p><strong>Note:</strong> This was a dry run. No ads were actually paused. ' +
'Set CONFIG.dryRun = false to apply these changes.</p>';
}
MailApp.sendEmail({
to: CONFIG.emailAddresses,
subject: subject,
htmlBody: body
});
}
This script has helped my clients achieve:
You can extend this script to:
Have you implemented systematic ad testing in your Google Ads accounts? What results have you seen? 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.