Production-ready prompts, scripts, frameworks and AI agents for Google Ads professionals. No payment required.
Sudden drops are easy to spot. Gradual declines, a 2% CPA increase per week over 6 weeks, go unnoticed until you’ve wasted thousands. This agent catches them early.
var CONFIG = {
// ═══════════════════════════════════════════════════════════════════════════
// DETECTION SETTINGS
// ═══════════════════════════════════════════════════════════════════════════
RECENT_WINDOW: 7, // Days for "current" performance
BASELINE_WINDOW: 30, // Days for baseline comparison
MIN_CONVERSIONS: 5, // Skip campaigns with too few conversions
// Thresholds (percentage change to trigger alert)
THRESHOLDS: {
CPA_INCREASE: 20, // CPA up 20%+ vs baseline
CONV_RATE_DROP: 15, // Conv rate down 15%+ vs baseline
CTR_DROP: 20, // CTR down 20%+ vs baseline
ROAS_DROP: 20, // ROAS down 20%+ vs baseline
},
// ═══════════════════════════════════════════════════════════════════════════
// NOTIFICATIONS
// ═══════════════════════════════════════════════════════════════════════════
EMAIL_RECIPIENTS: [],
SLACK_WEBHOOK_URL: '',
SPREADSHEET_URL: 'CREATE_NEW',
};
function main() {
var recent = getPeriodData(CONFIG.RECENT_WINDOW);
var baseline = getPeriodData(CONFIG.BASELINE_WINDOW);
var alerts = [];
Object.keys(recent).forEach(function(campaign) {
var r = recent[campaign];
var b = baseline[campaign];
if (!b || b.conversions < CONFIG.MIN_CONVERSIONS) return;
var checks = [];
// CPA check
if (r.conversions > 0 && b.conversions > 0) {
var cpaCurrent = r.cost / r.conversions;
var cpaBaseline = b.cost / b.conversions;
var cpaChange = ((cpaCurrent - cpaBaseline) / cpaBaseline) * 100;
if (cpaChange >= CONFIG.THRESHOLDS.CPA_INCREASE) {
checks.push({
metric: 'CPA',
baseline: '$' + cpaBaseline.toFixed(2),
current: '$' + cpaCurrent.toFixed(2),
change: '+' + cpaChange.toFixed(1) + '%',
severity: cpaChange >= CONFIG.THRESHOLDS.CPA_INCREASE * 2 ? 'high' : 'medium',
});
}
}
// Conversion rate check
if (r.clicks > 50 && b.clicks > 50) {
var crCurrent = r.conversions / r.clicks * 100;
var crBaseline = b.conversions / b.clicks * 100;
var crChange = ((crBaseline - crCurrent) / crBaseline) * 100;
if (crChange >= CONFIG.THRESHOLDS.CONV_RATE_DROP) {
checks.push({
metric: 'Conv. Rate',
baseline: crBaseline.toFixed(2) + '%',
current: crCurrent.toFixed(2) + '%',
change: '-' + crChange.toFixed(1) + '%',
severity: crChange >= CONFIG.THRESHOLDS.CONV_RATE_DROP * 2 ? 'high' : 'medium',
});
}
}
// CTR check
if (r.impressions > 100 && b.impressions > 100) {
var ctrCurrent = r.clicks / r.impressions * 100;
var ctrBaseline = b.clicks / b.impressions * 100;
var ctrChange = ((ctrBaseline - ctrCurrent) / ctrBaseline) * 100;
if (ctrChange >= CONFIG.THRESHOLDS.CTR_DROP) {
checks.push({
metric: 'CTR',
baseline: ctrBaseline.toFixed(2) + '%',
current: ctrCurrent.toFixed(2) + '%',
change: '-' + ctrChange.toFixed(1) + '%',
severity: 'medium',
});
}
}
if (checks.length > 0) {
alerts.push({ campaign: campaign, issues: checks });
}
});
if (alerts.length > 0) {
logResults(alerts);
sendNotifications(alerts);
}
Logger.log('Done. ' + alerts.length + ' campaigns with degradation detected.');
}
function getPeriodData(days) {
var end = new Date();
var start = new Date();
start.setDate(start.getDate() - days);
var fmt = function(d) {
return Utilities.formatDate(d, AdsApp.currentAccount().getTimeZone(), 'yyyyMMdd');
};
var query = 'SELECT CampaignName, Impressions, Clicks, Cost, Conversions ' +
'FROM CAMPAIGN_PERFORMANCE_REPORT ' +
'WHERE CampaignStatus = ENABLED ' +
'DURING ' + fmt(start) + ',' + fmt(end);
var rows = AdsApp.report(query).rows();
var data = {};
while (rows.hasNext()) {
var row = rows.next();
var name = row['CampaignName'];
if (!data[name]) {
data[name] = { impressions: 0, clicks: 0, cost: 0, conversions: 0 };
}
data[name].impressions += parseInt(row['Impressions']);
data[name].clicks += parseInt(row['Clicks']);
data[name].cost += parseFloat(row['Cost']);
data[name].conversions += parseFloat(row['Conversions']);
}
return data;
}
function logResults(alerts) {
var ss;
if (CONFIG.SPREADSHEET_URL === 'CREATE_NEW') {
ss = SpreadsheetApp.create('Performance Trends ' + new Date().toISOString().split('T')[0]);
} else {
ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
}
var sheet = ss.getActiveSheet();
if (sheet.getLastRow() === 0) {
sheet.appendRow(['Date', 'Campaign', 'Metric', 'Baseline', 'Current', 'Change', 'Severity']);
}
var today = new Date().toISOString().split('T')[0];
alerts.forEach(function(a) {
a.issues.forEach(function(i) {
sheet.appendRow([today, a.campaign, i.metric, i.baseline, i.current, i.change, i.severity]);
});
});
}
function sendNotifications(alerts) {
var totalIssues = alerts.reduce(function(sum, a) { return sum + a.issues.length; }, 0);
var subject = 'Performance Alert: ' + totalIssues + ' degradations in ' + alerts.length + ' campaigns';
var body = alerts.map(function(a) {
var lines = a.campaign + ':';
a.issues.forEach(function(i) {
lines += '\n [' + i.severity.toUpperCase() + '] ' + i.metric +
' ' + i.baseline + ' → ' + i.current + ' (' + i.change + ')';
});
return lines;
}).join('\n\n');
CONFIG.EMAIL_RECIPIENTS.forEach(function(email) {
MailApp.sendEmail(email, subject, body);
});
if (CONFIG.SLACK_WEBHOOK_URL) {
UrlFetchApp.fetch(CONFIG.SLACK_WEBHOOK_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ text: '*' + subject + '*\n```\n' + body + '\n```' }),
});
}
}
| Metric | Comparison | Default Alert Threshold |
|---|---|---|
| CPA | 7-day vs 30-day | +20% increase |
| Conversion Rate | 7-day vs 30-day | -15% drop |
| CTR | 7-day vs 30-day | -20% drop |
| ROAS | 7-day vs 30-day | -20% drop |
A 7-day window is long enough to smooth daily variance but short enough to catch trends within 1-2 weeks. The 30-day baseline represents “normal” performance. If 7-day metrics diverge from the 30-day norm, something changed.