Skip to content

Instantly share code, notes, and snippets.

@mbaersch
Last active October 20, 2020 08:36
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mbaersch/276f6a821e0770ce596141cece98a750 to your computer and use it in GitHub Desktop.
Save mbaersch/276f6a821e0770ce596141cece98a750 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;
}
@mbaersch
Copy link
Author

mbaersch commented Jan 9, 2020

Hinweis zum Update vom Januar 2020
Da nach wie vor kein Zugriff auf Smart Shopping Kampagnen via Ads Scripts besteht, wohl aber in den Performance Reports Daten zu diesen Kampagnen abgerufen werden können, ist als Workaround eine Möglichkeit geschaffen worden, Namen von Smart Shopping Kampagnen selbst zu definieren, wenn diese ebenfalls untersucht werden sollen.

Dazu können Namen von Smart Shopping Kampagnen als Array in der Variable setupSmartCampaignNames eintragen werden, 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 werden, wenn auch das Produkt deaktiviert sein mag.

Das kann wieder umgestellt werden, wenn denn irgendwann auch Zugriff auf die Smart Kampagnen besteht, aber zum aktuellen Zeitpunkt sind diese nach wie vor "unsichtbar", wenn man generell Kampagnen oder gezielt Shopping Kampagnen oder Shopping Anzeigengruppen abrufen möchte - Sorry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment