Production-ready prompts, scripts, frameworks and AI agents for Google Ads professionals. No payment required.
/******************************************************************************
* PMAX ASSET PERFORMANCE DUMP
*
* Generated by PPC.io Script Engine
* https://ppc.io
*
* Purpose: Extract all Performance Max asset group performance data
* Author: PPC.io
* Version: 2.0
* Updated: 2025-01-13
*
* SETUP INSTRUCTIONS:
* 1. Set SPREADSHEET_URL to 'CREATE_NEW' or paste existing URL
* 2. Run in Preview mode first to verify access
* 3. Schedule: Weekly recommended
*
* USE CASE: "Which assets should I replace? Give me specific recommendations."
*
* OUTPUTS:
* - 1. Summary: Overview with AI prompts
* - 2. Asset Groups: Performance by asset group
* - 3. Text Assets: Headlines, descriptions with Performance Score
* - 4. Image Assets: Image performance ratings
* - 5. Video Assets: Video performance ratings
*
* CHANGELOG:
* v2.0 - Added numbered sheets, Performance Score, GAQL-first with fallback
* v1.0 - Initial release
*
******************************************************************************/
/******************************************************************************
* CONFIGURATION - Adjust these values for your account
******************************************************************************/
var CONFIG = {
// ═══════════════════════════════════════════════════════════════════════════
// OUTPUT SETTINGS
// ═══════════════════════════════════════════════════════════════════════════
SPREADSHEET_URL: 'CREATE_NEW', // Or paste existing spreadsheet URL
// Email alerts (leave empty array to disable)
EMAIL_RECIPIENTS: [],
// Slack webhook (leave empty to disable)
SLACK_WEBHOOK_URL: '',
// ═══════════════════════════════════════════════════════════════════════════
// DATE RANGE
// ═══════════════════════════════════════════════════════════════════════════
DATE_RANGE: 'LAST_30_DAYS',
// ═══════════════════════════════════════════════════════════════════════════
// FILTERS
// ═══════════════════════════════════════════════════════════════════════════
CAMPAIGN_NAME_CONTAINS: '', // Filter campaigns (empty = all)
CAMPAIGN_NAME_EXCLUDES: '', // Exclude campaigns containing this
INCLUDE_PAUSED_CAMPAIGNS: false,
// ═══════════════════════════════════════════════════════════════════════════
// 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', 'PMax Asset Performance Dump started: ' + startTime.toISOString());
try {
var ss = initializeSpreadsheet();
var results = collectPMaxData(startTime);
writeAllSheets(ss, results);
sendNotifications(results, ss.getUrl(), startTime);
logSummary(results, startTime);
} catch (error) {
handleFatalError(error, startTime);
}
}
/******************************************************************************
* DATA COLLECTION
******************************************************************************/
function collectPMaxData(startTime) {
var results = {
campaigns: [],
assetGroups: [],
textAssets: [],
imageAssets: [],
videoAssets: [],
summary: {
totalCampaigns: 0,
totalAssetGroups: 0,
totalAssets: 0,
assetsByRating: {
'BEST': 0,
'GOOD': 0,
'LOW': 0,
'LEARNING': 0,
'UNKNOWN': 0
},
assetsToReplace: []
}
};
// Get PMax campaigns
results.campaigns = getPMaxCampaigns(startTime);
results.summary.totalCampaigns = results.campaigns.length;
// Get asset groups
results.assetGroups = getAssetGroups(startTime);
results.summary.totalAssetGroups = results.assetGroups.length;
// Get assets with performance
var assets = getAssetPerformance(startTime);
results.textAssets = assets.text;
results.imageAssets = assets.images;
results.videoAssets = assets.videos;
// Calculate summary
var allAssets = results.textAssets.concat(results.imageAssets).concat(results.videoAssets);
results.summary.totalAssets = allAssets.length;
for (var i = 0; i < allAssets.length; i++) {
var rating = allAssets[i]['Performance Rating'] || 'UNKNOWN';
if (results.summary.assetsByRating[rating] !== undefined) {
results.summary.assetsByRating[rating]++;
} else {
results.summary.assetsByRating['UNKNOWN']++;
}
// Flag assets to replace (LOW rating with significant impressions)
if (rating === 'LOW' && allAssets[i].Impressions >= 1000) {
results.summary.assetsToReplace.push({
type: allAssets[i]['Asset Type'],
content: allAssets[i]['Asset Text'] || allAssets[i]['Asset URL'] || 'N/A',
campaign: allAssets[i].Campaign,
impressions: allAssets[i].Impressions
});
}
}
// Sort by impressions
results.summary.assetsToReplace.sort(function(a, b) { return b.impressions - a.impressions; });
log('INFO', 'Collected PMax data: ' + results.summary.totalCampaigns + ' campaigns, ' +
results.summary.totalAssetGroups + ' asset groups, ' + results.summary.totalAssets + ' assets');
return results;
}
function getPMaxCampaigns(startTime) {
var campaigns = [];
var query = "SELECT " +
"campaign.id, " +
"campaign.name, " +
"campaign.status, " +
"campaign.bidding_strategy_type, " +
"campaign_budget.amount_micros, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.cost_micros, " +
"metrics.conversions, " +
"metrics.conversions_value " +
"FROM campaign " +
"WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
if (!CONFIG.INCLUDE_PAUSED_CAMPAIGNS) {
query += " AND campaign.status = 'ENABLED'";
}
try {
var report = AdsApp.report(query);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
// Apply filters
if (CONFIG.CAMPAIGN_NAME_CONTAINS &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_CONTAINS.toLowerCase()) === -1) {
continue;
}
if (CONFIG.CAMPAIGN_NAME_EXCLUDES &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_EXCLUDES.toLowerCase()) !== -1) {
continue;
}
var costMicros = parseInt(row['metrics.cost_micros'], 10) || 0;
var cost = costMicros / 1000000;
var conversions = parseFloat(row['metrics.conversions']) || 0;
var convValue = parseFloat(row['metrics.conversions_value']) || 0;
var budgetMicros = parseInt(row['campaign_budget.amount_micros'], 10) || 0;
campaigns.push({
'Campaign ID': row['campaign.id'],
'Campaign Name': campaignName,
'Status': row['campaign.status'],
'Bidding Strategy': row['campaign.bidding_strategy_type'],
'Daily Budget': budgetMicros / 1000000,
'Impressions': parseInt(row['metrics.impressions'], 10) || 0,
'Clicks': parseInt(row['metrics.clicks'], 10) || 0,
'Cost': cost,
'Conversions': conversions,
'Conv Value': convValue,
'CPA': conversions > 0 ? cost / conversions : null,
'ROAS': cost > 0 ? convValue / cost : null
});
}
} catch (e) {
log('ERROR', 'Failed to fetch PMax campaigns: ' + e.message);
}
// Sort by cost
campaigns.sort(function(a, b) { return b.Cost - a.Cost; });
return campaigns;
}
function getAssetGroups(startTime) {
var assetGroups = [];
var query = "SELECT " +
"campaign.name, " +
"asset_group.id, " +
"asset_group.name, " +
"asset_group.status, " +
"asset_group.primary_status, " +
"asset_group.primary_status_reasons, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.cost_micros, " +
"metrics.conversions, " +
"metrics.conversions_value " +
"FROM asset_group " +
"WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
if (!CONFIG.INCLUDE_PAUSED_CAMPAIGNS) {
query += " AND campaign.status = 'ENABLED'";
}
try {
var report = AdsApp.report(query);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
// Apply filters
if (CONFIG.CAMPAIGN_NAME_CONTAINS &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_CONTAINS.toLowerCase()) === -1) {
continue;
}
if (CONFIG.CAMPAIGN_NAME_EXCLUDES &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_EXCLUDES.toLowerCase()) !== -1) {
continue;
}
var costMicros = parseInt(row['metrics.cost_micros'], 10) || 0;
var cost = costMicros / 1000000;
var conversions = parseFloat(row['metrics.conversions']) || 0;
var convValue = parseFloat(row['metrics.conversions_value']) || 0;
assetGroups.push({
'Campaign': campaignName,
'Asset Group ID': row['asset_group.id'],
'Asset Group Name': row['asset_group.name'],
'Status': row['asset_group.status'],
'Primary Status': row['asset_group.primary_status'],
'Status Reasons': row['asset_group.primary_status_reasons'] || '-',
'Impressions': parseInt(row['metrics.impressions'], 10) || 0,
'Clicks': parseInt(row['metrics.clicks'], 10) || 0,
'Cost': cost,
'Conversions': conversions,
'Conv Value': convValue,
'CPA': conversions > 0 ? cost / conversions : null,
'ROAS': cost > 0 ? convValue / cost : null
});
checkTimeLimit(startTime);
}
} catch (e) {
log('ERROR', 'Failed to fetch asset groups: ' + e.message);
}
// Sort by cost
assetGroups.sort(function(a, b) { return b.Cost - a.Cost; });
return assetGroups;
}
function getAssetPerformance(startTime) {
var assets = {
text: [],
images: [],
videos: []
};
// Get asset performance from asset_group_asset table
var query = "SELECT " +
"campaign.name, " +
"asset_group.name, " +
"asset.id, " +
"asset.name, " +
"asset.type, " +
"asset.text_asset.text, " +
"asset.image_asset.full_size.url, " +
"asset.youtube_video_asset.youtube_video_id, " +
"asset_group_asset.status, " +
"asset_group_asset.performance_label, " +
"asset_group_asset.field_type " +
"FROM asset_group_asset " +
"WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX'";
if (!CONFIG.INCLUDE_PAUSED_CAMPAIGNS) {
query += " AND campaign.status = 'ENABLED'";
}
try {
var report = AdsApp.report(query);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
// Apply filters
if (CONFIG.CAMPAIGN_NAME_CONTAINS &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_CONTAINS.toLowerCase()) === -1) {
continue;
}
if (CONFIG.CAMPAIGN_NAME_EXCLUDES &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_EXCLUDES.toLowerCase()) !== -1) {
continue;
}
var assetType = row['asset.type'];
var fieldType = row['asset_group_asset.field_type'] || '';
var performanceLabel = row['asset_group_asset.performance_label'] || 'UNKNOWN';
var baseAsset = {
'Campaign': campaignName,
'Asset Group': row['asset_group.name'],
'Asset ID': row['asset.id'],
'Asset Name': row['asset.name'] || '-',
'Asset Type': assetType,
'Field Type': fieldType,
'Status': row['asset_group_asset.status'],
'Performance Rating': performanceLabel,
'Impressions': 0, // Note: Per-asset impressions not available via this query
'AI Recommendation': getAssetRecommendation(performanceLabel, assetType)
};
// Categorize by asset type
if (assetType === 'TEXT') {
baseAsset['Asset Text'] = row['asset.text_asset.text'] || '';
baseAsset['Character Count'] = baseAsset['Asset Text'].length;
assets.text.push(baseAsset);
} else if (assetType === 'IMAGE') {
baseAsset['Asset URL'] = row['asset.image_asset.full_size.url'] || '';
assets.images.push(baseAsset);
} else if (assetType === 'YOUTUBE_VIDEO') {
var videoId = row['asset.youtube_video_asset.youtube_video_id'] || '';
baseAsset['Video ID'] = videoId;
baseAsset['Video URL'] = videoId ? 'https://youtube.com/watch?v=' + videoId : '';
assets.videos.push(baseAsset);
}
checkTimeLimit(startTime);
}
} catch (e) {
log('ERROR', 'Failed to fetch asset performance: ' + e.message);
// Try alternative approach
getAssetPerformanceFallback(assets, startTime);
}
return assets;
}
function getAssetPerformanceFallback(assets, startTime) {
log('INFO', 'Using fallback method for asset collection');
// Use GAQL to get asset performance with metrics
var queries = [
{
type: 'HEADLINE',
query: "SELECT campaign.name, asset_group.name, asset.text_asset.text, " +
"asset_group_asset.performance_label, asset_group_asset.status " +
"FROM asset_group_asset " +
"WHERE asset_group_asset.field_type = 'HEADLINE' " +
"AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'"
},
{
type: 'DESCRIPTION',
query: "SELECT campaign.name, asset_group.name, asset.text_asset.text, " +
"asset_group_asset.performance_label, asset_group_asset.status " +
"FROM asset_group_asset " +
"WHERE asset_group_asset.field_type = 'DESCRIPTION' " +
"AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'"
},
{
type: 'LONG_HEADLINE',
query: "SELECT campaign.name, asset_group.name, asset.text_asset.text, " +
"asset_group_asset.performance_label, asset_group_asset.status " +
"FROM asset_group_asset " +
"WHERE asset_group_asset.field_type = 'LONG_HEADLINE' " +
"AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'"
}
];
for (var q = 0; q < queries.length; q++) {
try {
var report = AdsApp.report(queries[q].query);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
if (CONFIG.CAMPAIGN_NAME_CONTAINS &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_CONTAINS.toLowerCase()) === -1) {
continue;
}
if (CONFIG.CAMPAIGN_NAME_EXCLUDES &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_EXCLUDES.toLowerCase()) !== -1) {
continue;
}
var performanceLabel = row['asset_group_asset.performance_label'] || 'UNKNOWN';
assets.text.push({
'Campaign': campaignName,
'Asset Group': row['asset_group.name'],
'Asset Type': 'TEXT',
'Field Type': queries[q].type,
'Asset Text': row['asset.text_asset.text'] || '',
'Character Count': (row['asset.text_asset.text'] || '').length,
'Status': row['asset_group_asset.status'],
'Performance Rating': performanceLabel,
'AI Recommendation': getAssetRecommendation(performanceLabel, 'TEXT')
});
}
} catch (e) {
log('DEBUG', 'Fallback query failed for ' + queries[q].type + ': ' + e.message);
}
}
}
function getAssetRecommendation(performanceLabel, assetType) {
var recommendations = {
'BEST': 'Keep - Top performer. Consider creating variations.',
'GOOD': 'Keep - Solid performance. Monitor for changes.',
'LOW': 'Replace - Underperforming. Test new creative.',
'LEARNING': 'Monitor - Still gathering data.',
'PENDING': 'Wait - Under review.',
'UNKNOWN': 'Check - Performance data not available.',
'UNSPECIFIED': 'Check - Performance data not available.'
};
return recommendations[performanceLabel] || 'Check manually';
}
/******************************************************************************
* 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 PMax Assets - ' +
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) {
// Summary sheet first (position 0)
writeSummarySheet(ss, results);
// Numbered sheets for clear navigation
if (results.assetGroups.length > 0) {
writeSheet(ss, '2. Asset Groups', results.assetGroups, Object.keys(results.assetGroups[0]));
}
if (results.textAssets.length > 0) {
writeSheet(ss, '3. Text Assets', results.textAssets, Object.keys(results.textAssets[0]));
}
if (results.imageAssets.length > 0) {
writeSheet(ss, '4. Image Assets', results.imageAssets, Object.keys(results.imageAssets[0]));
}
if (results.videoAssets.length > 0) {
writeSheet(ss, '5. Video Assets', results.videoAssets, Object.keys(results.videoAssets[0]));
}
if (results.campaigns.length > 0) {
writeSheet(ss, '6. PMax Campaigns', results.campaigns, Object.keys(results.campaigns[0]));
}
}
function writeSummarySheet(ss, results) {
var sheet = ss.getSheetByName('1. Summary');
if (!sheet) {
sheet = ss.insertSheet('1. Summary', 0);
} else {
sheet.clear();
}
var data = [
['PMAX ASSET PERFORMANCE DUMP', ''],
['Generated by PPC.io Script Engine', ''],
['https://ppc.io', ''],
['', ''],
['Account: ' + AdsApp.currentAccount().getName(), ''],
['Date Range: ' + CONFIG.DATE_RANGE, ''],
['Export Date: ' + new Date().toISOString(), ''],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['OVERVIEW', ''],
['═══════════════════════════════════════════════════════════════', ''],
['PMax Campaigns', results.summary.totalCampaigns],
['Asset Groups', results.summary.totalAssetGroups],
['Total Assets', results.summary.totalAssets],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['ASSET PERFORMANCE RATINGS', ''],
['═══════════════════════════════════════════════════════════════', ''],
['BEST (Top performers)', results.summary.assetsByRating['BEST']],
['GOOD (Solid performers)', results.summary.assetsByRating['GOOD']],
['LOW (Need replacement)', results.summary.assetsByRating['LOW']],
['LEARNING (Gathering data)', results.summary.assetsByRating['LEARNING']],
['UNKNOWN/Pending', results.summary.assetsByRating['UNKNOWN']],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['ASSETS TO REPLACE (LOW rating, 1000+ impressions)', ''],
['═══════════════════════════════════════════════════════════════', '']
];
var assetsToReplace = results.summary.assetsToReplace.slice(0, 20);
if (assetsToReplace.length === 0) {
data.push(['No assets flagged for replacement', '']);
} else {
for (var i = 0; i < assetsToReplace.length; i++) {
var asset = assetsToReplace[i];
var displayContent = asset.content.length > 50 ? asset.content.substring(0, 50) + '...' : asset.content;
data.push([
asset.type + ': ' + displayContent,
'Campaign: ' + asset.campaign
]);
}
}
data.push(['', '']);
data.push(['═══════════════════════════════════════════════════════════════', '']);
data.push(['AI ANALYSIS PROMPTS', '']);
data.push(['═══════════════════════════════════════════════════════════════', '']);
data.push(['Paste the asset sheets into Claude with these prompts:', '']);
data.push(['', '']);
data.push(['1. "Which assets should I replace? Give me specific recommendations."', '']);
data.push(['2. "Analyze the BEST performing assets. What patterns make them work?"', '']);
data.push(['3. "Compare asset performance across asset groups. Any consistency issues?"', '']);
data.push(['4. "Based on top performers, suggest 5 new headlines and descriptions to test."', '']);
data.push(['5. "Are there enough assets in each asset group? Flag any that need more."', '']);
sheet.getRange(1, 1, data.length, 2).setValues(data);
sheet.getRange(1, 1).setFontWeight('bold').setFontSize(14);
sheet.setColumnWidth(1, 500);
sheet.setColumnWidth(2, 300);
}
function writeSheet(ss, sheetName, data, columns) {
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);
}
// Format columns
formatColumn(sheet, columns, 'Cost', '$#,##0.00', rows.length);
formatColumn(sheet, columns, 'CPA', '$#,##0.00', rows.length);
formatColumn(sheet, columns, 'Conv Value', '$#,##0.00', rows.length);
formatColumn(sheet, columns, 'ROAS', '0.00', rows.length);
formatColumn(sheet, columns, 'Daily Budget', '$#,##0.00', rows.length);
// Color code performance ratings
applyPerformanceFormatting(sheet, columns, rows.length);
for (var col = 1; col <= Math.min(columns.length, 10); col++) {
sheet.autoResizeColumn(col);
}
}
function applyPerformanceFormatting(sheet, headers, numRows) {
var ratingCol = headers.indexOf('Performance Rating') + 1;
if (ratingCol === 0) return;
var range = sheet.getRange(2, ratingCol, numRows, 1);
var values = range.getValues();
var colors = {
'BEST': '#d4edda', // Green
'GOOD': '#cce5ff', // Blue
'LOW': '#f8d7da', // Red
'LEARNING': '#fff3cd', // Yellow
'UNKNOWN': '#e9ecef' // Gray
};
var bgColors = values.map(function(row) {
return [colors[row[0]] || '#ffffff'];
});
range.setBackgrounds(bgColors);
}
function formatColumn(sheet, headers, columnName, format, numRows) {
var colIndex = headers.indexOf(columnName);
if (colIndex !== -1) {
sheet.getRange(2, colIndex + 1, numRows, 1).setNumberFormat(format);
}
}
/******************************************************************************
* NOTIFICATION FUNCTIONS
******************************************************************************/
function sendNotifications(results, spreadsheetUrl, startTime) {
var duration = ((new Date() - startTime) / 1000).toFixed(1);
var message = [
'PMax Asset Performance Dump Complete',
'',
'Account: ' + AdsApp.currentAccount().getName(),
'Duration: ' + duration + 's',
'',
'Summary:',
'- PMax Campaigns: ' + results.summary.totalCampaigns,
'- Asset Groups: ' + results.summary.totalAssetGroups,
'- Total Assets: ' + results.summary.totalAssets,
'',
'Asset Ratings:',
'- BEST: ' + results.summary.assetsByRating['BEST'],
'- GOOD: ' + results.summary.assetsByRating['GOOD'],
'- LOW (replace): ' + results.summary.assetsByRating['LOW'],
'',
'Report: ' + spreadsheetUrl,
'',
'--',
'Generated by PPC.io Script Engine'
].join('\n');
if (CONFIG.EMAIL_RECIPIENTS && CONFIG.EMAIL_RECIPIENTS.length > 0) {
try {
MailApp.sendEmail({
to: CONFIG.EMAIL_RECIPIENTS.join(','),
subject: '[PPC.io] PMax Assets - ' + results.summary.assetsByRating['LOW'] + ' to replace',
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: ':rocket: *PPC.io PMax Assets*\n```' + message + '```'
})
});
log('INFO', 'Slack sent');
} catch (e) {
log('ERROR', 'Failed to send Slack: ' + e.message);
}
}
}
/******************************************************************************
* UTILITY FUNCTIONS
******************************************************************************/
function checkTimeLimit(startTime) {
var elapsed = (new Date() - startTime) / 1000 / 60;
if (elapsed > CONFIG.TIME_LIMIT_MINUTES) {
throw new Error('TIME_LIMIT: Processed up to current batch 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', 'PMAX ASSET DUMP COMPLETE');
log('INFO', 'Duration: ' + duration + ' seconds');
log('INFO', 'Campaigns: ' + results.summary.totalCampaigns);
log('INFO', 'Asset Groups: ' + results.summary.totalAssetGroups);
log('INFO', 'Total Assets: ' + results.summary.totalAssets);
log('INFO', 'Assets to Replace: ' + results.summary.assetsByRating['LOW']);
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] PMax Asset Dump Failed',
body: 'Error: ' + 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');
}