// Documentation: https://adssystem.store/scripts/spend-anomaly-alert/ // Writes: Nothing. Read-only. // Spend anomaly alert // Compares yesterday's cost per campaign against the same weekday over the // previous four weeks, so weekend and weekday patterns do not raise false alarms. // FROM campaign covers every channel type, including Performance Max and Demand Gen. // Read-only: it alerts, it does not pause anything. var THRESHOLD = 0.4; // 0.4 = alert at 40% above or below the baseline var MIN_COST = 10; // skip campaigns too small to matter var EMAIL = ''; // leave empty to log only function main() { var tz = AdsApp.currentAccount().getTimeZone(); var yesterday = daysAgo(1, tz); var baseline = [daysAgo(8, tz), daysAgo(15, tz), daysAgo(22, tz), daysAgo(29, tz)]; var today = costByCampaign(yesterday, yesterday); var past = {}; for (var i = 0; i < baseline.length; i++) { var day = costByCampaign(baseline[i], baseline[i]); for (var n1 in day) { if (!past[n1]) { past[n1] = []; } past[n1].push(day[n1]); } } var alerts = []; for (var name in today) { var samples = past[name] || []; if (samples.length < 2) { continue; } // too new to have a baseline var avg = 0; for (var j = 0; j < samples.length; j++) { avg += samples[j]; } avg = avg / samples.length; if (avg === 0) { continue; } if (avg < MIN_COST && today[name] < MIN_COST) { continue; } var delta = (today[name] - avg) / avg; if (Math.abs(delta) >= THRESHOLD) { alerts.push([name, today[name], avg, delta]); } } if (alerts.length === 0) { Logger.log('No campaign moved more than ' + (THRESHOLD * 100) + '% on ' + yesterday + '.'); return; } alerts.sort(function (a, b) { return Math.abs(b[3]) - Math.abs(a[3]); }); var lines = ['campaign\tyesterday\tbaseline\tchange']; for (var k = 0; k < alerts.length; k++) { var a = alerts[k]; lines.push(a[0] + '\t' + a[1].toFixed(2) + '\t' + a[2].toFixed(2) + '\t' + (a[3] > 0 ? '+' : '') + (a[3] * 100).toFixed(0) + '%'); } var body = AdsApp.currentAccount().getName() + '\n' + 'Spend on ' + yesterday + ' against the same weekday, previous four weeks.\n\n' + lines.join('\n'); Logger.log(body); if (EMAIL) { MailApp.sendEmail(EMAIL, 'Spend anomaly - ' + AdsApp.currentAccount().getName(), body); } } function costByCampaign(from, to) { var out = {}; var rows = AdsApp.search( 'SELECT campaign.name, metrics.cost_micros ' + 'FROM campaign ' + 'WHERE segments.date BETWEEN "' + from + '" AND "' + to + '" ' + ' AND campaign.status != "REMOVED"' ); while (rows.hasNext()) { var r = rows.next(); out[String(r.campaign.name)] = Number(r.metrics.costMicros) / 1000000; } return out; } function daysAgo(n, tz) { var d = new Date(); d.setDate(d.getDate() - n); return Utilities.formatDate(d, tz, 'yyyy-MM-dd'); }