Production-ready prompts, scripts, frameworks and AI agents for Google Ads professionals. No payment required.
/******************************************************************************
* EXTENSION PERFORMANCE AUDIT
*
* Generated by PPC.io Script Engine
* https://ppc.io
*
* Purpose: Audit all ad extensions for performance and coverage gaps
* Author: PPC.io
* Version: 1.0
* Updated: 2025-01-14
*
* SETUP INSTRUCTIONS:
* 1. Set SPREADSHEET_URL to 'CREATE_NEW' or paste existing URL
* 2. Configure filters if needed
* 3. Run in Preview mode first to verify
* 4. Schedule: Monthly recommended
*
* USE CASE: "Which sitelinks are getting the most clicks? What campaigns are missing extensions?"
*
* OUTPUTS:
* - 1. Summary: Extension coverage, top performers, issues
* - 2. Sitelink Performance: All sitelinks with performance metrics
* - 3. All Extensions: Complete inventory by type
* - 4. Campaign Coverage: Which campaigns have which extensions
* - 5. Recommendations: Missing extensions, underperformers
* - 6. Issues: Disapproved or limited extensions
*
* CHANGELOG:
* v1.0 - Initial release with GAQL-first architecture
*
******************************************************************************/
/******************************************************************************
* CONFIGURATION - Adjust these values for your account
******************************************************************************/
var CONFIG = {
// ═══════════════════════════════════════════════════════════════════════════
// OUTPUT SETTINGS
// ═══════════════════════════════════════════════════════════════════════════
SPREADSHEET_URL: 'CREATE_NEW', // Or paste existing spreadsheet URL
EMAIL_RECIPIENTS: [], // ['email@example.com']
SLACK_WEBHOOK_URL: '', // Slack incoming webhook URL
// ═══════════════════════════════════════════════════════════════════════════
// DATE RANGE
// ═══════════════════════════════════════════════════════════════════════════
DATE_RANGE: 'LAST_30_DAYS',
// ═══════════════════════════════════════════════════════════════════════════
// FILTERS
// ═══════════════════════════════════════════════════════════════════════════
CAMPAIGN_NAME_CONTAINS: '', // Filter to campaigns containing this
CAMPAIGN_NAME_EXCLUDES: '', // Exclude campaigns containing this
INCLUDE_PAUSED: false, // Include paused campaigns/entities
MINIMUM_IMPRESSIONS: 100, // Minimum impressions for performance data
// ═══════════════════════════════════════════════════════════════════════════
// EXTENSION TYPES TO ANALYZE
// ═══════════════════════════════════════════════════════════════════════════
ANALYZE_SITELINKS: true,
ANALYZE_CALLOUTS: true,
ANALYZE_SNIPPETS: true,
ANALYZE_CALLS: true,
ANALYZE_LOCATIONS: true,
ANALYZE_PRICES: true,
ANALYZE_PROMOTIONS: true,
ANALYZE_IMAGES: true,
// ═══════════════════════════════════════════════════════════════════════════
// THRESHOLDS
// ═══════════════════════════════════════════════════════════════════════════
MIN_CTR_FOR_GOOD: 0.05, // 5% CTR = good sitelink
MIN_CLICKS_FOR_ANALYSIS: 10, // Minimum clicks to evaluate performance
RECOMMENDED_SITELINKS: 4, // Minimum recommended sitelinks per campaign
RECOMMENDED_CALLOUTS: 4, // Minimum recommended callouts
RECOMMENDED_SNIPPETS: 2, // Minimum recommended structured snippets
// ═══════════════════════════════════════════════════════════════════════════
// EXECUTION SETTINGS
// ═══════════════════════════════════════════════════════════════════════════
LOG_LEVEL: 'INFO', // DEBUG, INFO, WARN, ERROR
TIME_LIMIT_MINUTES: 25, // Exit gracefully before this (max 30)
BATCH_SIZE: 500 // Rows per spreadsheet write
};
/******************************************************************************
* MAIN EXECUTION
******************************************************************************/
function main() {
var startTime = new Date();
log('INFO', 'Extension Performance Audit started: ' + startTime.toISOString());
try {
var ss = initializeSpreadsheet();
var results = auditExtensions(startTime);
writeAllSheets(ss, results);
sendNotifications(results, ss.getUrl(), startTime);
logSummary(results, startTime);
} catch (error) {
handleFatalError(error, startTime);
}
}
/******************************************************************************
* DATA COLLECTION & ANALYSIS
******************************************************************************/
function auditExtensions(startTime) {
var results = {
campaigns: [],
sitelinks: [],
callouts: [],
snippets: [],
calls: [],
locations: [],
prices: [],
promotions: [],
images: [],
allExtensions: [],
campaignCoverage: [],
recommendations: [],
issues: [],
summary: {
totalCampaigns: 0,
campaignsWithSitelinks: 0,
campaignsWithCallouts: 0,
campaignsWithSnippets: 0,
campaignsWithCalls: 0,
campaignsWithLocations: 0,
totalSitelinks: 0,
totalCallouts: 0,
totalSnippets: 0,
totalIssues: 0,
topPerformingSitelink: null,
avgSitelinkCTR: 0
}
};
// Get all campaigns first
log('INFO', 'Collecting campaign data...');
results.campaigns = getCampaigns(startTime);
results.summary.totalCampaigns = results.campaigns.length;
log('INFO', 'Found ' + results.campaigns.length + ' campaigns');
// Collect all extension types
if (CONFIG.ANALYZE_SITELINKS) {
log('INFO', 'Collecting sitelinks...');
collectSitelinks(results, startTime);
}
if (CONFIG.ANALYZE_CALLOUTS) {
log('INFO', 'Collecting callouts...');
collectCallouts(results, startTime);
}
if (CONFIG.ANALYZE_SNIPPETS) {
log('INFO', 'Collecting structured snippets...');
collectSnippets(results, startTime);
}
if (CONFIG.ANALYZE_CALLS) {
log('INFO', 'Collecting call extensions...');
collectCallExtensions(results, startTime);
}
if (CONFIG.ANALYZE_IMAGES) {
log('INFO', 'Collecting image extensions...');
collectImageExtensions(results, startTime);
}
// Build campaign coverage matrix
log('INFO', 'Building coverage matrix...');
buildCoverageMatrix(results);
// Generate recommendations
log('INFO', 'Generating recommendations...');
generateRecommendations(results);
return results;
}
function getCampaigns(startTime) {
var campaigns = [];
try {
var query = 'SELECT ' +
'campaign.id, ' +
'campaign.name, ' +
'campaign.status, ' +
'campaign.advertising_channel_type, ' +
'metrics.impressions, ' +
'metrics.clicks, ' +
'metrics.cost_micros, ' +
'metrics.conversions ' +
'FROM campaign ' +
'WHERE campaign.advertising_channel_type IN ("SEARCH", "DISPLAY") ' +
'AND segments.date DURING ' + CONFIG.DATE_RANGE;
if (!CONFIG.INCLUDE_PAUSED) {
query += ' AND campaign.status = "ENABLED"';
}
var rows = AdsApp.search(query);
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
if (!passesCampaignFilter(campaignName)) continue;
campaigns.push({
id: row['campaign.id'],
name: campaignName,
status: row['campaign.status'],
type: row['campaign.advertising_channel_type'],
impressions: parseInt(row['metrics.impressions'] || 0, 10),
clicks: parseInt(row['metrics.clicks'] || 0, 10),
cost: parseInt(row['metrics.cost_micros'] || 0, 10) / 1000000,
conversions: parseFloat(row['metrics.conversions'] || 0),
extensions: {
sitelinks: [],
callouts: [],
snippets: [],
calls: [],
locations: [],
prices: [],
promotions: [],
images: []
}
});
}
} catch (e) {
log('WARN', 'GAQL campaign fetch failed: ' + e.message);
campaigns = getCampaignsFallback(startTime);
}
return campaigns;
}
function getCampaignsFallback(startTime) {
var campaigns = [];
var selector = AdsApp.campaigns()
.withCondition('AdvertisingChannelType IN [SEARCH, DISPLAY]');
if (!CONFIG.INCLUDE_PAUSED) {
selector = selector.withCondition('Status = ENABLED');
}
var iterator = selector.get();
while (iterator.hasNext()) {
var campaign = iterator.next();
var campaignName = campaign.getName();
if (!passesCampaignFilter(campaignName)) continue;
var stats = campaign.getStatsFor(CONFIG.DATE_RANGE);
campaigns.push({
id: campaign.getId(),
name: campaignName,
status: campaign.isEnabled() ? 'ENABLED' : 'PAUSED',
type: campaign.getAdvertisingChannelType(),
impressions: stats.getImpressions(),
clicks: stats.getClicks(),
cost: stats.getCost(),
conversions: stats.getConversions(),
extensions: {
sitelinks: [],
callouts: [],
snippets: [],
calls: [],
locations: [],
prices: [],
promotions: [],
images: []
}
});
checkTimeLimit(startTime);
}
return campaigns;
}
/******************************************************************************
* SITELINK COLLECTION
******************************************************************************/
function collectSitelinks(results, startTime) {
var sitelinkMap = {};
// Try GAQL first for performance data
try {
var query = 'SELECT ' +
'campaign.id, ' +
'campaign.name, ' +
'asset.id, ' +
'asset.name, ' +
'asset.sitelink_asset.link_text, ' +
'asset.sitelink_asset.description1, ' +
'asset.sitelink_asset.description2, ' +
'asset.final_urls, ' +
'metrics.impressions, ' +
'metrics.clicks, ' +
'metrics.cost_micros, ' +
'metrics.conversions ' +
'FROM campaign_asset ' +
'WHERE asset.type = "SITELINK" ' +
'AND segments.date DURING ' + CONFIG.DATE_RANGE;
if (!CONFIG.INCLUDE_PAUSED) {
query += ' AND campaign.status = "ENABLED"';
}
var rows = AdsApp.search(query);
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
if (!passesCampaignFilter(campaignName)) continue;
var assetId = row['asset.id'];
var linkText = row['asset.sitelink_asset.link_text'] || '';
var impressions = parseInt(row['metrics.impressions'] || 0, 10);
var clicks = parseInt(row['metrics.clicks'] || 0, 10);
var cost = parseInt(row['metrics.cost_micros'] || 0, 10) / 1000000;
var conversions = parseFloat(row['metrics.conversions'] || 0);
var key = assetId + '|' + linkText;
if (!sitelinkMap[key]) {
sitelinkMap[key] = {
id: assetId,
linkText: linkText,
description1: row['asset.sitelink_asset.description1'] || '',
description2: row['asset.sitelink_asset.description2'] || '',
finalUrl: row['asset.final_urls'] || '',
campaigns: [],
impressions: 0,
clicks: 0,
cost: 0,
conversions: 0
};
}
sitelinkMap[key].campaigns.push(campaignName);
sitelinkMap[key].impressions += impressions;
sitelinkMap[key].clicks += clicks;
sitelinkMap[key].cost += cost;
sitelinkMap[key].conversions += conversions;
// Track in campaign
var campaign = findCampaign(results.campaigns, row['campaign.id']);
if (campaign) {
campaign.extensions.sitelinks.push(linkText);
}
}
log('INFO', 'GAQL: Found ' + Object.keys(sitelinkMap).length + ' sitelinks');
} catch (e) {
log('WARN', 'GAQL sitelink fetch failed, using fallback: ' + e.message);
collectSitelinksFallback(results, sitelinkMap, startTime);
}
// Process sitelink map into results
var totalCTR = 0;
var sitelinksWithClicks = 0;
for (var key in sitelinkMap) {
var sl = sitelinkMap[key];
var ctr = sl.impressions > 0 ? (sl.clicks / sl.impressions) : 0;
var convRate = sl.clicks > 0 ? (sl.conversions / sl.clicks) : 0;
var performanceScore = calculatePerformanceScore(sl.clicks, sl.impressions, sl.conversions);
var rating = 'UNKNOWN';
if (sl.clicks >= CONFIG.MIN_CLICKS_FOR_ANALYSIS) {
if (ctr >= CONFIG.MIN_CTR_FOR_GOOD) {
rating = 'GOOD';
} else if (ctr >= CONFIG.MIN_CTR_FOR_GOOD / 2) {
rating = 'AVERAGE';
} else {
rating = 'LOW';
}
} else if (sl.impressions > 0) {
rating = 'LEARNING';
}
var sitelinkData = {
linkText: sl.linkText,
description1: sl.description1,
description2: sl.description2,
finalUrl: sl.finalUrl,
campaigns: sl.campaigns.length,
campaignNames: sl.campaigns.slice(0, 5).join(', ') + (sl.campaigns.length > 5 ? '...' : ''),
impressions: sl.impressions,
clicks: sl.clicks,
cost: sl.cost,
conversions: sl.conversions,
ctr: ctr,
convRate: convRate,
performanceScore: performanceScore,
rating: rating
};
results.sitelinks.push(sitelinkData);
results.allExtensions.push({
type: 'Sitelink',
text: sl.linkText,
campaigns: sl.campaigns.length,
impressions: sl.impressions,
clicks: sl.clicks,
ctr: ctr,
performanceScore: performanceScore,
rating: rating
});
if (sl.clicks > 0) {
totalCTR += ctr;
sitelinksWithClicks++;
}
}
// Sort by performance score
results.sitelinks.sort(function(a, b) { return b.performanceScore - a.performanceScore; });
// Update summary
results.summary.totalSitelinks = results.sitelinks.length;
results.summary.avgSitelinkCTR = sitelinksWithClicks > 0 ? (totalCTR / sitelinksWithClicks) : 0;
if (results.sitelinks.length > 0) {
results.summary.topPerformingSitelink = results.sitelinks[0].linkText;
}
// Count campaigns with sitelinks
var campaignsWithSitelinks = 0;
for (var i = 0; i < results.campaigns.length; i++) {
if (results.campaigns[i].extensions.sitelinks.length > 0) {
campaignsWithSitelinks++;
}
}
results.summary.campaignsWithSitelinks = campaignsWithSitelinks;
log('INFO', 'Processed ' + results.sitelinks.length + ' sitelinks');
}
function collectSitelinksFallback(results, sitelinkMap, startTime) {
try {
var sitelinks = AdsApp.extensions().sitelinks().get();
while (sitelinks.hasNext()) {
var sl = sitelinks.next();
var linkText = sl.getLinkText();
var key = sl.getId() + '|' + linkText;
if (!sitelinkMap[key]) {
sitelinkMap[key] = {
id: sl.getId(),
linkText: linkText,
description1: sl.getDescription1() || '',
description2: sl.getDescription2() || '',
finalUrl: sl.urls().getFinalUrl() || '',
campaigns: [],
impressions: 0,
clicks: 0,
cost: 0,
conversions: 0
};
}
// Try to get stats
try {
var stats = sl.getStatsFor(CONFIG.DATE_RANGE);
sitelinkMap[key].impressions += stats.getImpressions();
sitelinkMap[key].clicks += stats.getClicks();
sitelinkMap[key].cost += stats.getCost();
sitelinkMap[key].conversions += stats.getConversions();
} catch (e) {
// Stats may not be available
}
}
} catch (e) {
log('WARN', 'Fallback sitelink collection failed: ' + e.message);
}
}
/******************************************************************************
* CALLOUT COLLECTION
******************************************************************************/
function collectCallouts(results, startTime) {
var calloutMap = {};
try {
var query = 'SELECT ' +
'campaign.id, ' +
'campaign.name, ' +
'asset.id, ' +
'asset.callout_asset.callout_text, ' +
'metrics.impressions, ' +
'metrics.clicks ' +
'FROM campaign_asset ' +
'WHERE asset.type = "CALLOUT" ' +
'AND segments.date DURING ' + CONFIG.DATE_RANGE;
if (!CONFIG.INCLUDE_PAUSED) {
query += ' AND campaign.status = "ENABLED"';
}
var rows = AdsApp.search(query);
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
if (!passesCampaignFilter(campaignName)) continue;
var calloutText = row['asset.callout_asset.callout_text'] || '';
var assetId = row['asset.id'];
var key = assetId + '|' + calloutText;
if (!calloutMap[key]) {
calloutMap[key] = {
id: assetId,
text: calloutText,
campaigns: [],
impressions: 0
};
}
calloutMap[key].campaigns.push(campaignName);
calloutMap[key].impressions += parseInt(row['metrics.impressions'] || 0, 10);
var campaign = findCampaign(results.campaigns, row['campaign.id']);
if (campaign) {
campaign.extensions.callouts.push(calloutText);
}
}
} catch (e) {
log('WARN', 'GAQL callout fetch failed, using fallback: ' + e.message);
collectCalloutsFallback(results, calloutMap, startTime);
}
// Process callouts
for (var key in calloutMap) {
var co = calloutMap[key];
results.callouts.push({
text: co.text,
campaigns: co.campaigns.length,
campaignNames: co.campaigns.slice(0, 5).join(', '),
impressions: co.impressions
});
results.allExtensions.push({
type: 'Callout',
text: co.text,
campaigns: co.campaigns.length,
impressions: co.impressions,
clicks: '-',
ctr: '-',
performanceScore: '-',
rating: co.impressions > 0 ? 'ACTIVE' : 'NO DATA'
});
}
results.summary.totalCallouts = results.callouts.length;
// Count campaigns with callouts
var campaignsWithCallouts = 0;
for (var i = 0; i < results.campaigns.length; i++) {
if (results.campaigns[i].extensions.callouts.length > 0) {
campaignsWithCallouts++;
}
}
results.summary.campaignsWithCallouts = campaignsWithCallouts;
log('INFO', 'Processed ' + results.callouts.length + ' callouts');
}
function collectCalloutsFallback(results, calloutMap, startTime) {
try {
var callouts = AdsApp.extensions().callouts().get();
while (callouts.hasNext()) {
var co = callouts.next();
var text = co.getText();
var key = co.getId() + '|' + text;
if (!calloutMap[key]) {
calloutMap[key] = {
id: co.getId(),
text: text,
campaigns: [],
impressions: 0
};
}
}
} catch (e) {
log('WARN', 'Fallback callout collection failed: ' + e.message);
}
}
/******************************************************************************
* STRUCTURED SNIPPET COLLECTION
******************************************************************************/
function collectSnippets(results, startTime) {
var snippetMap = {};
try {
var query = 'SELECT ' +
'campaign.id, ' +
'campaign.name, ' +
'asset.id, ' +
'asset.structured_snippet_asset.header, ' +
'asset.structured_snippet_asset.values, ' +
'metrics.impressions ' +
'FROM campaign_asset ' +
'WHERE asset.type = "STRUCTURED_SNIPPET" ' +
'AND segments.date DURING ' + CONFIG.DATE_RANGE;
if (!CONFIG.INCLUDE_PAUSED) {
query += ' AND campaign.status = "ENABLED"';
}
var rows = AdsApp.search(query);
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
if (!passesCampaignFilter(campaignName)) continue;
var header = row['asset.structured_snippet_asset.header'] || '';
var values = row['asset.structured_snippet_asset.values'] || '';
var assetId = row['asset.id'];
var key = assetId + '|' + header;
if (!snippetMap[key]) {
snippetMap[key] = {
id: assetId,
header: header,
values: values,
campaigns: [],
impressions: 0
};
}
snippetMap[key].campaigns.push(campaignName);
snippetMap[key].impressions += parseInt(row['metrics.impressions'] || 0, 10);
var campaign = findCampaign(results.campaigns, row['campaign.id']);
if (campaign) {
campaign.extensions.snippets.push(header);
}
}
} catch (e) {
log('WARN', 'GAQL snippet fetch failed, using fallback: ' + e.message);
collectSnippetsFallback(results, snippetMap, startTime);
}
// Process snippets
for (var key in snippetMap) {
var sn = snippetMap[key];
results.snippets.push({
header: sn.header,
values: sn.values,
campaigns: sn.campaigns.length,
campaignNames: sn.campaigns.slice(0, 5).join(', '),
impressions: sn.impressions
});
results.allExtensions.push({
type: 'Structured Snippet',
text: sn.header + ': ' + sn.values,
campaigns: sn.campaigns.length,
impressions: sn.impressions,
clicks: '-',
ctr: '-',
performanceScore: '-',
rating: sn.impressions > 0 ? 'ACTIVE' : 'NO DATA'
});
}
results.summary.totalSnippets = results.snippets.length;
// Count campaigns with snippets
var campaignsWithSnippets = 0;
for (var i = 0; i < results.campaigns.length; i++) {
if (results.campaigns[i].extensions.snippets.length > 0) {
campaignsWithSnippets++;
}
}
results.summary.campaignsWithSnippets = campaignsWithSnippets;
log('INFO', 'Processed ' + results.snippets.length + ' snippets');
}
function collectSnippetsFallback(results, snippetMap, startTime) {
try {
var snippets = AdsApp.extensions().snippets().get();
while (snippets.hasNext()) {
var sn = snippets.next();
var header = sn.getHeader();
var key = sn.getId() + '|' + header;
if (!snippetMap[key]) {
snippetMap[key] = {
id: sn.getId(),
header: header,
values: sn.getValues().join(', '),
campaigns: [],
impressions: 0
};
}
}
} catch (e) {
log('WARN', 'Fallback snippet collection failed: ' + e.message);
}
}
/******************************************************************************
* CALL EXTENSION COLLECTION
******************************************************************************/
function collectCallExtensions(results, startTime) {
var callMap = {};
try {
var query = 'SELECT ' +
'campaign.id, ' +
'campaign.name, ' +
'asset.id, ' +
'asset.call_asset.phone_number, ' +
'asset.call_asset.country_code, ' +
'metrics.impressions, ' +
'metrics.clicks ' +
'FROM campaign_asset ' +
'WHERE asset.type = "CALL" ' +
'AND segments.date DURING ' + CONFIG.DATE_RANGE;
if (!CONFIG.INCLUDE_PAUSED) {
query += ' AND campaign.status = "ENABLED"';
}
var rows = AdsApp.search(query);
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
if (!passesCampaignFilter(campaignName)) continue;
var phoneNumber = row['asset.call_asset.phone_number'] || '';
var countryCode = row['asset.call_asset.country_code'] || '';
var assetId = row['asset.id'];
var key = assetId + '|' + phoneNumber;
if (!callMap[key]) {
callMap[key] = {
id: assetId,
phoneNumber: phoneNumber,
countryCode: countryCode,
campaigns: [],
impressions: 0,
clicks: 0
};
}
callMap[key].campaigns.push(campaignName);
callMap[key].impressions += parseInt(row['metrics.impressions'] || 0, 10);
callMap[key].clicks += parseInt(row['metrics.clicks'] || 0, 10);
var campaign = findCampaign(results.campaigns, row['campaign.id']);
if (campaign) {
campaign.extensions.calls.push(phoneNumber);
}
}
} catch (e) {
log('WARN', 'GAQL call extension fetch failed: ' + e.message);
collectCallExtensionsFallback(results, callMap, startTime);
}
// Process call extensions
for (var key in callMap) {
var call = callMap[key];
var ctr = call.impressions > 0 ? (call.clicks / call.impressions) : 0;
results.calls.push({
phoneNumber: call.phoneNumber,
countryCode: call.countryCode,
campaigns: call.campaigns.length,
campaignNames: call.campaigns.slice(0, 5).join(', '),
impressions: call.impressions,
clicks: call.clicks,
ctr: ctr
});
results.allExtensions.push({
type: 'Call',
text: call.phoneNumber,
campaigns: call.campaigns.length,
impressions: call.impressions,
clicks: call.clicks,
ctr: ctr,
performanceScore: '-',
rating: call.clicks > 0 ? 'ACTIVE' : (call.impressions > 0 ? 'LOW' : 'NO DATA')
});
}
// Count campaigns with calls
var campaignsWithCalls = 0;
for (var i = 0; i < results.campaigns.length; i++) {
if (results.campaigns[i].extensions.calls.length > 0) {
campaignsWithCalls++;
}
}
results.summary.campaignsWithCalls = campaignsWithCalls;
log('INFO', 'Processed ' + results.calls.length + ' call extensions');
}
function collectCallExtensionsFallback(results, callMap, startTime) {
try {
var phones = AdsApp.extensions().phoneNumbers().get();
while (phones.hasNext()) {
var ph = phones.next();
var phoneNumber = ph.getPhoneNumber();
var key = ph.getId() + '|' + phoneNumber;
if (!callMap[key]) {
callMap[key] = {
id: ph.getId(),
phoneNumber: phoneNumber,
countryCode: ph.getCountry(),
campaigns: [],
impressions: 0,
clicks: 0
};
}
}
} catch (e) {
log('WARN', 'Fallback call extension collection failed: ' + e.message);
}
}
/******************************************************************************
* IMAGE EXTENSION COLLECTION
******************************************************************************/
function collectImageExtensions(results, startTime) {
try {
var query = 'SELECT ' +
'campaign.id, ' +
'campaign.name, ' +
'asset.id, ' +
'asset.image_asset.full_size.url, ' +
'metrics.impressions, ' +
'metrics.clicks ' +
'FROM campaign_asset ' +
'WHERE asset.type = "IMAGE" ' +
'AND segments.date DURING ' + CONFIG.DATE_RANGE;
if (!CONFIG.INCLUDE_PAUSED) {
query += ' AND campaign.status = "ENABLED"';
}
var rows = AdsApp.search(query);
var imageMap = {};
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
if (!passesCampaignFilter(campaignName)) continue;
var assetId = row['asset.id'];
var imageUrl = row['asset.image_asset.full_size.url'] || '';
if (!imageMap[assetId]) {
imageMap[assetId] = {
id: assetId,
url: imageUrl,
campaigns: [],
impressions: 0,
clicks: 0
};
}
imageMap[assetId].campaigns.push(campaignName);
imageMap[assetId].impressions += parseInt(row['metrics.impressions'] || 0, 10);
imageMap[assetId].clicks += parseInt(row['metrics.clicks'] || 0, 10);
var campaign = findCampaign(results.campaigns, row['campaign.id']);
if (campaign) {
campaign.extensions.images.push(assetId);
}
}
for (var id in imageMap) {
var img = imageMap[id];
var ctr = img.impressions > 0 ? (img.clicks / img.impressions) : 0;
results.images.push({
id: img.id,
url: img.url,
campaigns: img.campaigns.length,
impressions: img.impressions,
clicks: img.clicks,
ctr: ctr
});
results.allExtensions.push({
type: 'Image',
text: 'Image ' + img.id,
campaigns: img.campaigns.length,
impressions: img.impressions,
clicks: img.clicks,
ctr: ctr,
performanceScore: '-',
rating: img.clicks > 0 ? 'ACTIVE' : (img.impressions > 0 ? 'LOW' : 'NO DATA')
});
}
log('INFO', 'Processed ' + results.images.length + ' image extensions');
} catch (e) {
log('DEBUG', 'Image extension fetch failed (may not be supported): ' + e.message);
}
}
/******************************************************************************
* COVERAGE MATRIX & RECOMMENDATIONS
******************************************************************************/
function buildCoverageMatrix(results) {
for (var i = 0; i < results.campaigns.length; i++) {
var campaign = results.campaigns[i];
// Deduplicate extension arrays
campaign.extensions.sitelinks = uniqueArray(campaign.extensions.sitelinks);
campaign.extensions.callouts = uniqueArray(campaign.extensions.callouts);
campaign.extensions.snippets = uniqueArray(campaign.extensions.snippets);
campaign.extensions.calls = uniqueArray(campaign.extensions.calls);
var sitelinkCount = campaign.extensions.sitelinks.length;
var calloutCount = campaign.extensions.callouts.length;
var snippetCount = campaign.extensions.snippets.length;
var callCount = campaign.extensions.calls.length;
var totalExtensions = sitelinkCount + calloutCount + snippetCount + callCount;
var healthScore = 0;
if (sitelinkCount >= CONFIG.RECOMMENDED_SITELINKS) healthScore += 25;
else if (sitelinkCount > 0) healthScore += 10;
if (calloutCount >= CONFIG.RECOMMENDED_CALLOUTS) healthScore += 25;
else if (calloutCount > 0) healthScore += 10;
if (snippetCount >= CONFIG.RECOMMENDED_SNIPPETS) healthScore += 25;
else if (snippetCount > 0) healthScore += 10;
if (callCount > 0) healthScore += 25;
var status = healthScore >= 75 ? 'GOOD' : healthScore >= 50 ? 'FAIR' : 'POOR';
results.campaignCoverage.push({
'Campaign': campaign.name,
'Campaign Type': campaign.type,
'Sitelinks': sitelinkCount,
'Callouts': calloutCount,
'Snippets': snippetCount,
'Calls': callCount,
'Total Extensions': totalExtensions,
'Health Score': healthScore,
'Status': status,
'Impressions': campaign.impressions,
'Clicks': campaign.clicks
});
}
// Sort by health score ascending (worst first)
results.campaignCoverage.sort(function(a, b) {
return a['Health Score'] - b['Health Score'];
});
}
function generateRecommendations(results) {
// Check for campaigns missing extensions
for (var i = 0; i < results.campaigns.length; i++) {
var campaign = results.campaigns[i];
if (campaign.extensions.sitelinks.length < CONFIG.RECOMMENDED_SITELINKS) {
results.recommendations.push({
campaign: campaign.name,
type: 'Missing Sitelinks',
current: campaign.extensions.sitelinks.length,
recommended: CONFIG.RECOMMENDED_SITELINKS,
impact: 'HIGH',
action: 'Add ' + (CONFIG.RECOMMENDED_SITELINKS - campaign.extensions.sitelinks.length) + ' more sitelinks'
});
}
if (campaign.extensions.callouts.length < CONFIG.RECOMMENDED_CALLOUTS) {
results.recommendations.push({
campaign: campaign.name,
type: 'Missing Callouts',
current: campaign.extensions.callouts.length,
recommended: CONFIG.RECOMMENDED_CALLOUTS,
impact: 'MEDIUM',
action: 'Add ' + (CONFIG.RECOMMENDED_CALLOUTS - campaign.extensions.callouts.length) + ' more callouts'
});
}
if (campaign.extensions.snippets.length < CONFIG.RECOMMENDED_SNIPPETS) {
results.recommendations.push({
campaign: campaign.name,
type: 'Missing Snippets',
current: campaign.extensions.snippets.length,
recommended: CONFIG.RECOMMENDED_SNIPPETS,
impact: 'MEDIUM',
action: 'Add ' + (CONFIG.RECOMMENDED_SNIPPETS - campaign.extensions.snippets.length) + ' more structured snippets'
});
}
}
// Check for underperforming sitelinks
for (var j = 0; j < results.sitelinks.length; j++) {
var sl = results.sitelinks[j];
if (sl.rating === 'LOW' && sl.clicks >= CONFIG.MIN_CLICKS_FOR_ANALYSIS) {
results.recommendations.push({
campaign: sl.campaignNames,
type: 'Low CTR Sitelink',
current: (sl.ctr * 100).toFixed(2) + '% CTR',
recommended: (CONFIG.MIN_CTR_FOR_GOOD * 100) + '% CTR',
impact: 'MEDIUM',
action: 'Review/replace sitelink: "' + sl.linkText + '"'
});
}
}
// Sort recommendations by impact
var impactOrder = { 'HIGH': 0, 'MEDIUM': 1, 'LOW': 2 };
results.recommendations.sort(function(a, b) {
return (impactOrder[a.impact] || 3) - (impactOrder[b.impact] || 3);
});
}
/******************************************************************************
* OUTPUT FUNCTIONS
******************************************************************************/
function initializeSpreadsheet() {
var ss;
if (!CONFIG.SPREADSHEET_URL || CONFIG.SPREADSHEET_URL === 'YOUR_SPREADSHEET_URL_HERE' || CONFIG.SPREADSHEET_URL === 'CREATE_NEW') {
ss = SpreadsheetApp.create('PPC.io Extension Audit - ' +
AdsApp.currentAccount().getName() + ' - ' +
formatDate(new Date()));
log('INFO', 'Created spreadsheet: ' + ss.getUrl());
} else {
ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
}
return ss;
}
function writeAllSheets(ss, results) {
// 1. Summary
writeSummarySheet(ss, results);
// 2. Sitelink Performance
if (results.sitelinks.length > 0) {
var sitelinkData = results.sitelinks.map(function(sl) {
return {
'Link Text': sl.linkText,
'Description 1': sl.description1,
'Description 2': sl.description2,
'Final URL': sl.finalUrl,
'Campaigns': sl.campaigns,
'Impressions': sl.impressions,
'Clicks': sl.clicks,
'CTR': (sl.ctr * 100).toFixed(2) + '%',
'Conversions': sl.conversions.toFixed(2),
'Conv Rate': (sl.convRate * 100).toFixed(2) + '%',
'Performance Score': sl.performanceScore.toFixed(1),
'Rating': sl.rating
};
});
writeSheet(ss, '2. Sitelink Performance', sitelinkData,
['Link Text', 'Description 1', 'Impressions', 'Clicks', 'CTR',
'Conversions', 'Conv Rate', 'Performance Score', 'Rating', 'Campaigns']);
}
// 3. All Extensions
if (results.allExtensions.length > 0) {
var extensionData = results.allExtensions.map(function(ext) {
return {
'Type': ext.type,
'Text': ext.text,
'Campaigns': ext.campaigns,
'Impressions': ext.impressions,
'Clicks': ext.clicks,
'CTR': ext.ctr !== '-' ? (ext.ctr * 100).toFixed(2) + '%' : '-',
'Rating': ext.rating
};
});
writeSheet(ss, '3. All Extensions', extensionData,
['Type', 'Text', 'Campaigns', 'Impressions', 'Clicks', 'CTR', 'Rating']);
}
// 4. Campaign Coverage
if (results.campaignCoverage.length > 0) {
writeSheet(ss, '4. Campaign Coverage', results.campaignCoverage,
['Campaign', 'Campaign Type', 'Sitelinks', 'Callouts', 'Snippets',
'Calls', 'Total Extensions', 'Health Score', 'Status']);
}
// 5. Recommendations
if (results.recommendations.length > 0) {
var recData = results.recommendations.map(function(rec) {
return {
'Campaign': rec.campaign,
'Issue': rec.type,
'Current': rec.current,
'Recommended': rec.recommended,
'Impact': rec.impact,
'Action': rec.action
};
});
writeSheet(ss, '5. Recommendations', recData,
['Campaign', 'Issue', 'Current', 'Recommended', 'Impact', 'Action']);
}
// 6. Issues (if any)
if (results.issues.length > 0) {
writeSheet(ss, '6. Issues', results.issues,
['Extension Type', 'Text', 'Issue', 'Campaign', 'Action Required']);
}
}
function writeSummarySheet(ss, results) {
var sheet = ss.getSheetByName('1. Summary');
if (!sheet) {
sheet = ss.insertSheet('1. Summary', 0);
} else {
sheet.clear();
}
var sitelinkCoverage = results.summary.totalCampaigns > 0 ?
((results.summary.campaignsWithSitelinks / results.summary.totalCampaigns) * 100).toFixed(1) : 0;
var calloutCoverage = results.summary.totalCampaigns > 0 ?
((results.summary.campaignsWithCallouts / results.summary.totalCampaigns) * 100).toFixed(1) : 0;
var snippetCoverage = results.summary.totalCampaigns > 0 ?
((results.summary.campaignsWithSnippets / results.summary.totalCampaigns) * 100).toFixed(1) : 0;
var data = [
['EXTENSION PERFORMANCE AUDIT', ''],
['Generated by PPC.io Script Engine', ''],
['https://ppc.io', ''],
['', ''],
['Account: ' + AdsApp.currentAccount().getName(), ''],
['Date Range: ' + CONFIG.DATE_RANGE, ''],
['Export Date: ' + new Date().toISOString(), ''],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['EXTENSION INVENTORY', ''],
['═══════════════════════════════════════════════════════════════', ''],
['Total Campaigns Analyzed', results.summary.totalCampaigns],
['', ''],
['Sitelinks', results.summary.totalSitelinks],
['Callouts', results.summary.totalCallouts],
['Structured Snippets', results.summary.totalSnippets],
['Call Extensions', results.calls.length],
['Image Extensions', results.images.length],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['COVERAGE ANALYSIS', ''],
['═══════════════════════════════════════════════════════════════', ''],
['Campaigns with Sitelinks', results.summary.campaignsWithSitelinks + ' (' + sitelinkCoverage + '%)'],
['Campaigns with Callouts', results.summary.campaignsWithCallouts + ' (' + calloutCoverage + '%)'],
['Campaigns with Snippets', results.summary.campaignsWithSnippets + ' (' + snippetCoverage + '%)'],
['Campaigns with Calls', results.summary.campaignsWithCalls],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['SITELINK PERFORMANCE', ''],
['═══════════════════════════════════════════════════════════════', ''],
['Top Performing Sitelink', results.summary.topPerformingSitelink || 'N/A'],
['Average Sitelink CTR', (results.summary.avgSitelinkCTR * 100).toFixed(2) + '%'],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['RECOMMENDATIONS SUMMARY', ''],
['═══════════════════════════════════════════════════════════════', ''],
['Total Recommendations', results.recommendations.length],
['High Priority', results.recommendations.filter(function(r) { return r.impact === 'HIGH'; }).length],
['Medium Priority', results.recommendations.filter(function(r) { return r.impact === 'MEDIUM'; }).length],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['BEST PRACTICES', ''],
['═══════════════════════════════════════════════════════════════', ''],
['Recommended Sitelinks per Campaign', CONFIG.RECOMMENDED_SITELINKS + ' minimum'],
['Recommended Callouts per Campaign', CONFIG.RECOMMENDED_CALLOUTS + ' minimum'],
['Recommended Snippets per Campaign', CONFIG.RECOMMENDED_SNIPPETS + ' minimum'],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['AI ANALYSIS PROMPTS', ''],
['═══════════════════════════════════════════════════════════════', ''],
['Copy extension data into Claude with these prompts:', ''],
['', ''],
['Prompt 1', '"Which sitelinks are getting the most clicks? Why?"'],
['Prompt 2', '"What campaigns are missing important extensions?"'],
['Prompt 3', '"How can I improve my extension coverage?"'],
['Prompt 4', '"What patterns do top-performing sitelinks share?"'],
['Prompt 5', '"Create an extension optimization priority list"']
];
sheet.getRange(1, 1, data.length, 2).setValues(data);
sheet.getRange(1, 1).setFontWeight('bold').setFontSize(14);
sheet.setColumnWidth(1, 400);
sheet.setColumnWidth(2, 300);
// Highlight coverage percentages
highlightCoverageMetrics(sheet, sitelinkCoverage, calloutCoverage, snippetCoverage);
}
function highlightCoverageMetrics(sheet, sitelinkCov, calloutCov, snippetCov) {
var data = sheet.getDataRange().getValues();
for (var i = 0; i < data.length; i++) {
if (data[i][0].indexOf('Campaigns with Sitelinks') === 0) {
var color = sitelinkCov >= 80 ? '#d4edda' : sitelinkCov >= 50 ? '#fff3cd' : '#f8d7da';
sheet.getRange(i + 1, 2).setBackground(color);
}
if (data[i][0].indexOf('Campaigns with Callouts') === 0) {
var color = calloutCov >= 80 ? '#d4edda' : calloutCov >= 50 ? '#fff3cd' : '#f8d7da';
sheet.getRange(i + 1, 2).setBackground(color);
}
if (data[i][0].indexOf('Campaigns with Snippets') === 0) {
var color = snippetCov >= 80 ? '#d4edda' : snippetCov >= 50 ? '#fff3cd' : '#f8d7da';
sheet.getRange(i + 1, 2).setBackground(color);
}
}
}
function writeSheet(ss, sheetName, data, columns) {
if (!data || data.length === 0) {
log('DEBUG', 'No data for sheet: ' + sheetName);
return;
}
var sheet = ss.getSheetByName(sheetName);
if (!sheet) {
sheet = ss.insertSheet(sheetName);
} else {
sheet.clear();
}
sheet.getRange(1, 1, 1, columns.length).setValues([columns]).setFontWeight('bold');
sheet.setFrozenRows(1);
var rows = data.map(function(row) {
return columns.map(function(col) {
var val = row[col];
return val !== null && val !== undefined ? val : '';
});
});
for (var i = 0; i < rows.length; i += CONFIG.BATCH_SIZE) {
var batch = rows.slice(i, Math.min(i + CONFIG.BATCH_SIZE, rows.length));
sheet.getRange(2 + i, 1, batch.length, columns.length).setValues(batch);
}
// Color code rating/status columns
applyRatingColors(sheet, columns, rows.length);
for (var col = 1; col <= Math.min(columns.length, 10); col++) {
sheet.autoResizeColumn(col);
}
log('DEBUG', 'Wrote ' + rows.length + ' rows to ' + sheetName);
}
function applyRatingColors(sheet, columns, numRows) {
var ratingCol = columns.indexOf('Rating') + 1;
var statusCol = columns.indexOf('Status') + 1;
var impactCol = columns.indexOf('Impact') + 1;
var colors = {
'GOOD': '#d4edda',
'AVERAGE': '#fff3cd',
'LOW': '#f8d7da',
'LEARNING': '#cce5ff',
'ACTIVE': '#d4edda',
'NO DATA': '#e9ecef',
'UNKNOWN': '#e9ecef',
'FAIR': '#fff3cd',
'POOR': '#f8d7da',
'HIGH': '#f8d7da',
'MEDIUM': '#fff3cd'
};
[ratingCol, statusCol, impactCol].forEach(function(colIndex) {
if (colIndex > 0 && numRows > 0) {
var range = sheet.getRange(2, colIndex, numRows, 1);
var values = range.getValues();
var bgColors = values.map(function(row) {
return [colors[row[0]] || '#ffffff'];
});
range.setBackgrounds(bgColors);
}
});
}
/******************************************************************************
* NOTIFICATION FUNCTIONS
******************************************************************************/
function sendNotifications(results, spreadsheetUrl, startTime) {
var duration = ((new Date() - startTime) / 1000).toFixed(1);
var message = [
'Extension Performance Audit Complete',
'',
'Account: ' + AdsApp.currentAccount().getName(),
'Date Range: ' + CONFIG.DATE_RANGE,
'Duration: ' + duration + 's',
'',
'Extension Inventory:',
'- Sitelinks: ' + results.summary.totalSitelinks,
'- Callouts: ' + results.summary.totalCallouts,
'- Snippets: ' + results.summary.totalSnippets,
'- Call Extensions: ' + results.calls.length,
'',
'Coverage:',
'- Campaigns with Sitelinks: ' + results.summary.campaignsWithSitelinks + '/' + results.summary.totalCampaigns,
'- Campaigns with Callouts: ' + results.summary.campaignsWithCallouts + '/' + results.summary.totalCampaigns,
'',
'Recommendations: ' + results.recommendations.length,
'',
'Report: ' + spreadsheetUrl,
'',
'--',
'Generated by PPC.io Script Engine'
].join('\n');
if (CONFIG.EMAIL_RECIPIENTS && CONFIG.EMAIL_RECIPIENTS.length > 0) {
try {
var urgency = results.recommendations.filter(function(r) { return r.impact === 'HIGH'; }).length;
var emoji = urgency > 5 ? '🔴' : urgency > 0 ? '🟡' : '🟢';
MailApp.sendEmail({
to: CONFIG.EMAIL_RECIPIENTS.join(','),
subject: '[PPC.io] Extension Audit ' + emoji + ' - ' + results.recommendations.length + ' recommendations',
body: message
});
log('INFO', 'Email sent');
} catch (e) {
log('ERROR', 'Failed to send email: ' + e.message);
}
}
if (CONFIG.SLACK_WEBHOOK_URL) {
try {
UrlFetchApp.fetch(CONFIG.SLACK_WEBHOOK_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({
text: ':electric_plug: *PPC.io Extension Audit*\n```' + message + '```'
})
});
log('INFO', 'Slack sent');
} catch (e) {
log('ERROR', 'Failed to send Slack: ' + e.message);
}
}
}
/******************************************************************************
* UTILITY FUNCTIONS
******************************************************************************/
function calculatePerformanceScore(clicks, impressions, conversions) {
if (impressions === 0) return 0;
var ctr = clicks / impressions;
var convRate = clicks > 0 ? conversions / clicks : 0;
return (ctr * 1000) + (convRate * 5000);
}
function findCampaign(campaigns, campaignId) {
for (var i = 0; i < campaigns.length; i++) {
if (campaigns[i].id == campaignId) {
return campaigns[i];
}
}
return null;
}
function uniqueArray(arr) {
var seen = {};
return arr.filter(function(item) {
if (seen[item]) return false;
seen[item] = true;
return true;
});
}
function passesCampaignFilter(campaignName) {
if (CONFIG.CAMPAIGN_NAME_CONTAINS &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_CONTAINS.toLowerCase()) === -1) {
return false;
}
if (CONFIG.CAMPAIGN_NAME_EXCLUDES &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_EXCLUDES.toLowerCase()) !== -1) {
return false;
}
return true;
}
function checkTimeLimit(startTime) {
var elapsed = (new Date() - startTime) / 1000 / 60;
if (elapsed > CONFIG.TIME_LIMIT_MINUTES) {
throw new Error('TIME_LIMIT: Audit stopped after ' + elapsed.toFixed(1) + ' minutes.');
}
}
function log(level, message) {
var levels = { 'DEBUG': 0, 'INFO': 1, 'WARN': 2, 'ERROR': 3 };
if (levels[level] >= levels[CONFIG.LOG_LEVEL]) {
Logger.log('[' + level + '] ' + message);
}
}
function logSummary(results, startTime) {
var duration = ((new Date() - startTime) / 1000).toFixed(1);
log('INFO', '════════════════════════════════════════');
log('INFO', 'EXTENSION AUDIT COMPLETE');
log('INFO', 'Duration: ' + duration + ' seconds');
log('INFO', 'Campaigns: ' + results.summary.totalCampaigns);
log('INFO', 'Sitelinks: ' + results.summary.totalSitelinks);
log('INFO', 'Callouts: ' + results.summary.totalCallouts);
log('INFO', 'Snippets: ' + results.summary.totalSnippets);
log('INFO', 'Recommendations: ' + results.recommendations.length);
log('INFO', '════════════════════════════════════════');
}
function handleFatalError(error, startTime) {
log('ERROR', '════════════════════════════════════════');
log('ERROR', 'FATAL ERROR: ' + error.message);
log('ERROR', 'Stack: ' + error.stack);
log('ERROR', '════════════════════════════════════════');
if (CONFIG.EMAIL_RECIPIENTS && CONFIG.EMAIL_RECIPIENTS.length > 0) {
try {
MailApp.sendEmail({
to: CONFIG.EMAIL_RECIPIENTS.join(','),
subject: '[PPC.io ERROR] Extension Audit Failed - ' + AdsApp.currentAccount().getName(),
body: 'Script failed after ' + ((new Date() - startTime) / 1000).toFixed(1) +
' seconds.\n\nError: ' + error.message + '\n\nStack:\n' + error.stack
});
} catch (e) {
log('ERROR', 'Could not send error email: ' + e.message);
}
}
}
function formatDate(date) {
return Utilities.formatDate(date, AdsApp.currentAccount().getTimeZone(), 'yyyy-MM-dd');
}