Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save klubbing/525147d27cde9e3dda2a24d76b65d489 to your computer and use it in GitHub Desktop.
Save klubbing/525147d27cde9e3dda2a24d76b65d489 to your computer and use it in GitHub Desktop.
Identifizieren von schlecht performenden Keywords und Produkten in AdWords
/**************************************************************/
/****** "Gurkenfinder"-Script für Google AdWords *******/
/**************************************************************/
/* v1.4 2020 Markus Baersch (@mbaersch)
Reduzierte Non-MCC-Fassung
gandke marketing & software - www.gandke.de */
/*********** Start Setup **********************/
var emailAddress = "mailadresse@hier.eintragen";
var emailName = "" ;
var chkLabel = "PC:DoCheck" ; //Nur Kampagnen mit diesem Label werden berücksichtigt
var chkPauseLabel = "PC:Paused" ; //Optionales Label für pausierte Keywords
//Standard- / Schwellwerte & Einstellungen
var chkType = 'REL'; //REL = Umsatz/Kosten, CNV = Anzahl Conversions; CPA = Kosten/Conversion
var trshClicks = 200; //Schwellwert für erzielte Klicks zur Untersuchung
var chkDateRange = 'ALL_TIME'; //Mögl. Werte: TODAY, YESTERDAY, LAST_7_DAYS, THIS_WEEK_SUN_TODAY,
//LAST_WEEK, LAST_14_DAYS, LAST_30_DAYS, LAST_BUSINESS_WEEK, LAST_WEEK_SUN_SAT,
//THIS_MONTH, LAST_MONTH, ALL_TIME
//REL
var trshCnvCostRelReport = 2; //Melden ab CVal/Cost unter 2
var trshCnvCostRelPause = 0.5; //Pausieren, wenn CVal/Cost unter 0,5
//CNV
var trshCnvReport = 2; //Melden bei weniger als 2 Conversions
var trshCnvPause = 0.5; //Pausieren unter 0,5 Conversions
//CPA
var trshCpaReport = 30; //Melden bei CPA über 30,--
var trshCpaPause = 50; //Pausieren bei CPA > 40,--
var setupDays = [1,4] ; //Wochentage; Sonntag = 0. Hier: nur Montags und Donnertags ausführen
//Mieser Workaround, weil immer noch kein Zugriff auf Smart Shopping Kampagnen via API besteht, wohl aber
//die Berichte genutzt werden können: Hier die Namen der Smart Shopping Kampagnen als Array eintragen,
//welche zusätzlich zu normalen Shopping Kampagnen untersucht werden sollen. Fundstellen werden dann ausgewiesen,
//aber es besteht keine Information darüber, ob das Produkt bereits deaktiviert wurde oder nicht.
//Daher speziell in Verbindung mit "ALl_TIME" als Datumsbereich nervig, weil Einträge nie aus dem Bericht
//verschwinden würden, wenn auch das Produkt deaktiviert sein mag.
var setupSmartCampaignNames = ['Smart Shopping Beispiel 1', 'Smart Shopping Beispiel 2'];
var debug = false ;
/*********** Ende Setup ***********************/
var Wochentage = new Array("Sonntag", "Montag", "Dienstag", "Mittwoch",
"Donnerstag", "Freitag", "Samstag");
function main() {
var chkShResults = new Array();
var chkKwResults = new Array();
//Start nur an ausgewählten Wochentagen
var wTag = getAccountCurrentDateTime().getDay();
if (debug || (setupDays.indexOf(wTag) >= 0)) {
w2log('Vorgang gestartet.') ;
var mandantId = AdWordsApp.currentAccount().getCustomerId();
var mandantName = AdWordsApp.currentAccount().getName();
//Labels anlegen, wenn nicht vorhanden
needsLabel(chkLabel) ;
if (chkPauseLabel) needsLabel(chkPauseLabel) ;
//Ergebnisse für Keyword- und Shopping-Kampagnen ermitteln
chkKwResults = checkKeywordPerformance();
chkShResults = checkProductPerformance();
if (debug) {
w2log(formatResults(chkShResults, 'S', true)) ;
w2log(formatResults(chkKwResults, 'K', true)) ;
}
w2log('Vorgang abgeschlossen.') ;
if ((!emailAddress) || (emailAddress == "mailadresse@hier.eintragen")) {
w2log("Es wurde keine Mailadresse angegeben, Report wird nicht versendet.");
return "";
} else {
//Mail mit Ergebnissen erstellen
if (!debug) {
if (emailName) emailName = ' ('+emailName+')' ;
var htmlResult = "<br><br><hr><h2>Ergebnisse für \"" + mandantName + "\"</h2>\n" + "\n" +
formatResults(chkShResults, 'S', false) + "\n" +
formatResults(chkKwResults, 'K', false) ;
var mlSubject = 'Gurkenfinder-Report'+ emailName +' für '+ Wochentage[wTag] + ', den '+
Utilities.formatDate(new Date(), AdWordsApp.currentAccount().getTimeZone(), "dd.MM.yyyy") ;
var resPreText = '<style type="text/css">body,small,h1,h2,h3,p,ul,li{font-size:11px;font-family:arial,sans-serif;color:#222}'+
'td{padding-right:13px;white-space:nowrap;vertical-align:top}b.r{color:red}h1,h2,h3{margin:20px 0 0 0;'+
'padding:0 0 2px 0;font-size:16px;color:#C44E00}h2 a{color:#C44E00}h1{font-size:18px}h3{font-size:13px;color:#14941D}</style>'+
'<h1>'+mlSubject+'</h1>' ;
var resBody = resPreText + htmlResult + '<p>llap!</p>' ;
if (resBody.length >= 200*1024) {
w2log("Report zu groß für E-Mail.") ;
//nicht senden...
resBody = resPreText + '<p>Der Report ist leider zu groß für den Versand per E-Mail.</p>' ;
w2log(formatResults(chkShResult, 'S', true)) ;
w2log(formatResults(chkKwResult, 'K', true)) ;
}
//Mail senden
MailApp.sendEmail({
to: emailAddress,
subject: mlSubject,
htmlBody: resBody,
});
w2log('Info wurde per Mail gesendet.');
}
}
} else
w2log('Heute setze ich aus...') ;
}
function checkKeywordPerformance() {
//Aktive Kampagnen abrufen
var campaignIterator = AdWordsApp.campaigns()
.withCondition("Status = ENABLED")
.withCondition("LabelNames CONTAINS_ALL ['"+chkLabel+"']")
.get();
var cmpFound = false;
var cResults = new Array();
//Keyword-Reports für ausgewählte Kampagnen abrufen
while (campaignIterator.hasNext()) {
cmpFound = true;
var campaign = campaignIterator.next();
var repStatement = 'SELECT Id, CampaignName, AdGroupName, Criteria, Clicks, '+
'Ctr, AveragePosition, AverageCpc, Cost, Conversions, CostPerConversion, ConversionValue ' +
'FROM KEYWORDS_PERFORMANCE_REPORT WHERE CampaignName="' + campaign.getName() +
'" AND Status = ENABLED AND Clicks > '+trshClicks ;
if (chkDateRange != "ALL_TIME") repStatement += ' DURING '+chkDateRange ;
var report = AdWordsApp.report(repStatement);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var cst = row['Cost'];
var cstraw = getValue(cst) ;
var rev = row['ConversionValue'];
var revraw = getValue(rev) ;
var rel = formatFloat(revraw / cstraw) ;
var cnvs = row['Conversions'] ;
var cpa = row['CostPerConversion'] ;
var cnvsraw = getValue(cnvs);
var cparaw = getValue(cpa) ;
if ((cnvsraw === 0) && (chkType === 'CPA')) cpa = 999999;
if (((chkType === 'REL') && (rel < trshCnvCostRelReport)) ||
((chkType === 'CPA') && (cparaw > trshCpaReport)) ||
((chkType === 'CNV') && (cnvsraw < trshCnvReport)))
{
var aktRow = new Array();
aktRow.push(row['CampaignName']);
aktRow.push(row['AdGroupName']);
if (((chkType === 'REL') && (rel < trshCnvCostRelPause)) ||
((chkType === 'CPA') && (cpa > trshCpaPause)) ||
((chkType === 'CNV') && (cnvs < trshCnvPause)))
{
var kw = AdWordsApp.keywords().withCondition('Id='+row['Id']).get().next();
if (chkPauseLabel != "") addLbl(kw, chkPauseLabel)
kw.pause();
aktRow.push('<b style="color:red">' + row['Criteria'] + ' [pausiert]</b>');
} else
aktRow.push(row['Criteria']);
aktRow.push(formatInt(row['Clicks']));
aktRow.push(row['Ctr']);
aktRow.push(row['AverageCpc']);
aktRow.push(cst);
aktRow.push(row['AveragePosition']);
aktRow.push(cnvs);
aktRow.push(cpa);
aktRow.push(row['ConversionValue']);
aktRow.push(rel);
cResults.push(aktRow) ;
}
} //Reportiterator
} //Kampagneniterator
if (!cmpFound) cResults = "NIX" ;
return cResults;
}
function checkProductPerformance() {
//Aktive Kampagnen abrufen
var campaignIterator = AdWordsApp.shoppingCampaigns()
.withCondition("Status = ENABLED")
.withCondition("LabelNames CONTAINS_ALL ['"+chkLabel+"']")
.get();
var cmpFound = false;
var cResults = new Array();
var campaignNames = [];
campaignNames.push(setupSmartCampaignNames);
while (campaignIterator.hasNext()) {
campaignNames.push([campaignIterator.next().getName()]);
}
//Keyword-Reports für ausgewählte Kampagnen abrufen
for (i=0; i<campaignNames.length; i++) {
cmpFound = true;
var campaign = campaignNames[i];
var repStatement = 'SELECT OfferId, Brand, ProductTypeL1, CategoryL1, AdGroupId, CampaignName, AdGroupName, Clicks, Ctr, AverageCpc, Cost, '+
'Conversions, CostPerConversion, ConversionValue ' +
'FROM SHOPPING_PERFORMANCE_REPORT WHERE CampaignName="' + campaign + '" AND Clicks > '+trshClicks ;
if (chkDateRange != "ALL_TIME") repStatement += ' DURING '+chkDateRange ;
var report = AdWordsApp.report(repStatement);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var adg = row['AdGroupId'];
var prdId = row['OfferId'];
var cst = row['Cost'];
var cstraw = getValue(cst) ;
var rev = row['ConversionValue'];
var revraw = getValue(rev) ;
var rel = formatFloat(revraw / cstraw) ;
var cnvs = row['Conversions'] ;
var cpa = row['CostPerConversion'] ;
var cnvsraw = getValue(cnvs);
var cparaw = getValue(cpa) ;
if ((cnvsraw === 0) && (chkType === 'CPA')) cpa = 999999;
if (((chkType === 'REL') && (rel < trshCnvCostRelReport)) ||
((chkType === 'CPA') && (cparaw > trshCpaReport)) ||
((chkType === 'CNV') && (cnvsraw < trshCnvReport)))
{
//Ist das Produkt noch aktiv oder schon deaktiviert?
var itemInactive = false ;
var adgrp = AdWordsApp.shoppingAdGroups().withIds([adg]).get();
//Ist es eine Anzeigengruppe einer normalen Shoping Kampagne? Dann status überprüfen.
//Bei Smart Shopping steht die Info derzeit nicht zur Verfügung
if (adgrp.totalNumEntities() > 0) {
var adgrps = adgrp.next().productGroups().get();
while (adgrps.hasNext()) {
var grp = adgrps.next();
if (grp.getDimension() == 'ITEM_ID')
if (grp.asItemId().getValue() == prdId)
if (grp.asItemId().isExcluded()) {
itemInactive = true ;
w2log('INFO: ' + prdId + ' übergangen, weil bereits deaktiviert');
break;
}
}
}
if (!itemInactive) {
var aktRow = new Array();
aktRow.push(row['CampaignName']);
aktRow.push(row['AdGroupName']);
if (((chkType === 'REL') && (rel < trshCnvCostRelPause)) ||
((chkType === 'CPA') && (cpa > trshCpaPause)) ||
((chkType === 'CNV') && (cnvs < trshCnvPause)))
{
aktRow.push('<b style="color:red">' + row['OfferId'] + ' - manuell pausieren!</b>');
} else
aktRow.push(row['OfferId']);
aktRow.push(row['Brand']);
aktRow.push(row['ProductTypeL1']);
aktRow.push(row['CategoryL1']);
aktRow.push(formatInt(row['Clicks']));
aktRow.push(row['Ctr']);
aktRow.push(row['AverageCpc']);
aktRow.push(row['Cost']);
aktRow.push(cnvs);
aktRow.push(cpa);
aktRow.push(row['ConversionValue']);
aktRow.push(rel);
cResults.push(aktRow) ;
}
}
} //Reportiterator
} //Kampagneniterator
if (!cmpFound) cResults = "NIX" ;
return cResults;
}
/**************** Helper ******************/
//Ergebnisse für Log oder Mail ausgeben
function formatResults(arr, tp, forLog) {
var res = "" ;
if (arr === 'NIX') return "";
var doHtmlTitle = true ;
if (tp === 'S') {
var nam = "Aktive Produkte";
var hdr = ['Kampagne', 'Anzeigengruppe', 'Produkt', 'Marke', 'Prod-Typ', 'Kategorie', 'Klicks', 'CTR', 'CPC', 'Kosten', 'Conv.', 'Kost./Conv.', 'Umsatz', 'Umsatz/Kosten'] ;
} else {
var nam = "Aktive Keywords";
var hdr = ['Kampagne', 'Anzeigengruppe', 'Keyword', 'Klicks', 'CTR', 'CPC', 'Kosten', 'Position', 'Conv.', 'Kost./Conv.', 'Umsatz', 'Umsatz/Kosten'] ;
}
if (chkType === 'REL')
var ttl = nam + " mit Verhältnis von Umsatz/Kosten unter "+trshCnvCostRelReport + ' und mindestens '+trshClicks+' Klicks' ;
else if (chkType === 'CPA')
var ttl = nam + " mit Kosten/Conversion über "+trshCpaReport + ' und mindestens '+trshClicks+' Klicks' ;
else
var ttl = nam + " mit weniger als "+trshCnvReport + ' Conversions und mindestens '+trshClicks+' Klicks' ;
ttl += " im Zeitraum "+chkDateRange;
if (forLog) {
res = "\n"+ttl + ":\n" ;
if (arr.length == 0) res += "- Keine Einträge -" ; else {
res += hdr.join('; ').replace(/<br>/g,"") + '\n';
for (var i=0;i<arr.length;i++) {
if(typeof arr[i] === 'string')
res += arr[i] + '\n';
else
res += arr[i].join('; ') + '\n';
}
}
} else {
if (doHtmlTitle) {
res = "\n<h3>" + ttl + "</h3>\n";
if (arr.length == 0) res += "<small>Keine Einträge</small>\n" ; else {
res += "<table>\n";
if (hdr.length > 0) res += "<tr><td class=\"first\"><b>"+ hdr.join('</b></td>\t<td><b>') + '</b>\t</td></tr>\n';
for (var i=0;i<arr.length;i++) {
if(typeof arr[i] === 'string')
res += "<tr><td class=\"first\">" + arr[i] + "</td>\t</tr>\n";
else
res += "<tr><td class=\"first\">"+arr[i].join('</td>\t<td>').replace(/\n/gm,"<br>") + '</td>\t</tr>\n';
}
res += "</table>\n";
}
}
}
return res ;
}
//angefordertes Label anlegen, wenn noch nicht vorhanden
function needsLabel(lbl) {
if (!(AdWordsApp.labels().withCondition("Name = '" + lbl + "'").get().hasNext())) {
AdWordsApp.createLabel(lbl, "Markiert Daten für Controlling / Steuerung per Script. " +
"Fragen dazu? Kontakt unter www.gandke.de)", "#C44E00");
//Da das Label hier ggf. schon direkt gebraucht wird, aber nicht zwingend schon genutzt werden kann, lieber noch ein wenig warten...
Utilities.sleep(1000);
}
return true ;
}
function w2log(txt) {
//Zu viele Details im Log bringen nichts - daher hier jeden Eintrag kürzen. Um Überlauf zu vermeiden ggf. hier als Ergänzung
//die Gesamtlänge aller Einträge beobachten und begrenzen
if (txt.length > (4096)) {
Logger.log(txt.substr(0, 4000)+"...\n\nACHTUNG:Text für Log gekürzt!!!") ;
return false;
} else {
try {Logger.log(txt) ;} catch(e) { }
return true;
}
}
//Ausgleich der ggf. bestehenden Zeitdifferenz zwischen Zeitstempel des
//ausf. Systems (PST) und Planungszeit im Konto
function getAccountCurrentDateTime() {
return new Date(Utilities.formatDate(new Date(), AdWordsApp.currentAccount().getTimeZone(), "MMM dd,yyyy HH:mm:ss"));
}
//Label ohne Fehlermeldungen in der Vorschau bei fehlendm Label hinzufügen
function addLbl(entity, lblname) {
if (!(entity.labels().withCondition("Name = '" + lblname + "'").get().hasNext()))
try { entity.applyLabel(lblname) ; } catch(e) { } ;
}
function getValue(v) {
return parseFloat(v.toString().replace(/,/g,''));
}
function formatInt(number) {
number = number || 0;
thousand = ",";
var negative = number < 0 ? "-" : "",
i = parseInt(number = Math.abs(+number || 0).toFixed(0), 10) + "",
j = (j = i.length) > 3 ? j % 3 : 0;
return negative + (j ? i.substr(0, j) + thousand : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousand) ;
}
function formatFloat(amount) {
var delimiter = ","; // replace comma if desired
amount = amount.toFixed(2).toString() ;
var a = amount.split('.',2)
var d = a[1];
var i = parseInt(a[0]);
if(isNaN(i)) { return ''; }
var minus = '';
if(i < 0) { minus = '-'; }
i = Math.abs(i);
var n = new String(i);
var a = [];
while(n.length > 3)
{
var nn = n.substr(n.length-3);
a.unshift(nn);
n = n.substr(0,n.length-3);
}
if(n.length > 0) { a.unshift(n); }
n = a.join(delimiter);
if(d.length < 1) { amount = n; }
else { amount = n + '.' + d; }
amount = minus + amount;
return amount;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment